Русский
Русский
English
Статистика
Реклама

Sms-шлюз

Свой личный SMS-шлюз. Часть 1 цели, задачи, сборка и тестирование

29.04.2021 12:05:53 | Автор: admin


Представляю вам цикл статей по созданию собственного шлюза для отправки SMS-сообщений.
В первой части мы определим цели и некоторые аспекты использования своего шлюза, настроим программное обеспечение для отправки SMS с использованием USB-модемов, а также рассмотрим несколько интересных вариантов отправки

Начнем мы статью с вопроса для чего отправлять SMS, ведь на дворе 2021 год? Да, мы уже привыкли к различным мессенджерам, уведомлениям в чат-боты, но SMS до сих пор обладает наиболее гарантированным уровнем доставки. Нет зависимости от наличия интернета и сообщение поступит даже в сети 2G и при слабом сигнале и на устройство без доступа к интернет. Таким образом, если планируется отправлять сообщения, которые гарантированно должны быть доставлены и критичны к времени доставки, то SMS это возможно лучший выбор.

Под SMS-шлюзом мы понимаем программно-аппаратное устройство подключенное к сети сотового оператора и позволяющее производить автоматизированную отправку сообщений.

Где можно применять это решение?


Самое простое и очевидное применение отправка уведомлений. Мы отслеживаем доступность различных сервисов и хотим знать о любых сбоях. Применение в системе умного дома позволит нам быть в курсе всех произошедших событиях. Да, многие IoT устройства умного дома оборудованы разъемом под сим-карту и могут сами отправлять SMS, но в этом случае на каждое устройство нужна отдельная сим-карта.
Мы можем проводить двухэтапную аутентификацию пользователей или производить валидацию введенного номера телефона. Регистрировать пользователей по номеру телефона, без необходимости создания логина и пароля. Отправлять сообщения о формировании заказа, отправке, получении, изменении баланса.

Но сообщения можно не только отправлять, их также можно получать и обрабатывать, что еще сильнее расширяет варианты применения. Все зависит только от вашей фантазии.

Стоит обратить особое внимание так как мы рассматриваем в статье вариант создания шлюза, то это позволяет отправлять сообщения сразу со всех описанных сервисов используя всего одно устройство.

Плюсы и минусы собственного шлюза


Как и любое решение, собственный шлюз обладает рядом достоинств и недостатков.
Мы не будем рассматривать плюсы с точки зрения точного сравнения с платными сервисами ввиду разного уровня получаемой услуги. Я приведу наиболее явные и вы сможете решить на сколько каждый пункт критичен именно для вас.

Плюс

  • Очень низкая цена за отправленное сообщение в районе 5 копеек за сообщение (при отправке 1000), против 2.65 на коммерческих шлюзах

Минусы

  • Отсутствует возможность задать имя в качестве отправителя будет номер телефона, а не Baton.ru, например. Но есть способ как решить это красиво и мы рассмотрим это в статье.
  • Отправка сообщения не чаще чем 1 раз в 10 секунд. Решается увеличением количества модемов.
  • Нет защиты от сбоев как на аппаратном, так и программном уровне. Объективности ради стоит отметить, что платные шлюзы тоже примерно раз в неделю сообщают о проблемах с доставкой определенным операторам. Мой опыт использования такого шлюза показывает всего один сбой за более чем 12 месяцев.

Итак, мы определили, что нам ожидать от собственного шлюза и мы готовы начать.

Устанавливаем Gammu, подключаем модемы


В качестве аппаратного ядра системы я буду использовать Orange Pi PC с Armbian просто потому, что он у меня есть и ничем не занят. Свою версию вы можете сделать на основе RPi, компьютера/сервера на Linux и даже виртуальной машины с проброшенными внутрь USB-портами это не имеет особого значения, главное мы будем использовать Linux.

Для общения с модемами я буду использовать Gammu очень мощный продукт позволяет общаться не только с классическими модемами, но и использовать телефоны в качестве модемов. Также этот продукт избавит нас от необходимости самим формировать пакеты сообщений, считать их длины, составлять и отправлять АТ-команды и самое главное позволить отправлять сообщения в PDU-формате.

Итак, подключимся к серверу, обновим систему и установим gammu:

ssh root@<IP_вашего_сервера>apt updateapt upgrade -yapt install gammu -y

Теперь нужно подключить модемы к USB-портам. Я буду использовать два модема, чтобы нагляднее показать чем отличаются настройки. Подключаем и смотрим какие порты они заняли.



Следующей командой мы смотрим, какой модем сколькими портами обладаем. Среди этих портов будет только несколько (или один), через который можно отправлять сообщения. Остальные это технические и нам не интересны.



На скрине выше видно, что установленный модем usb 5-1, Alcatel, имеет пять каналов. Теперь нам нужно определить какие из них используются для связи. Сделать это не сложно. Пишем в терминале:

screen /dev/ttyUSB1

В открывшемся окне вбиваем АТ и если в ответ получили ОК, то запоминаем этот порт это то, что нужно. Выходим CTRL+A затем D
Обратите внимание, что найдя один порт, все равно нужно проверить оставшиеся ttyUSB2, ttyUSB3, и далее. Например, используемый мной Alcatel, имеет 2 независимых канала на ttyUSB4 и ttyUSB5, что позволяет одновременно отправлять два SMS-сообщения через один модем.

Настройка Gammu


Итак мы определились с портами и теперь нужно настроить Gammu. Есть утилита конфигурации gammu-config, но мы не будем ее использовать, а запишем сразу данные в конфигурационный файл.

nano ~/.gammurc

дописываем в нижней части файла данные конфигураций. Каждый блок [gammu] это канал модема. [gammu] это канал по умолчанию, [gammu1] первый канал и т.д. Далее мы еще коснемся этого, когда будем отправлять сообщение. Итого я добавляю 3 доступных канала от двух своих модемов:

[gammu]port = /dev/ttyUSB0model = atconnection = atsynchronizetime = yeslogfile =logformat = nothinguse_locking = yesgammuloc =[gammu1]port = /dev/ttyUSB4model = atconnection = atsynchronizetime = yeslogfile =logformat = nothinguse_locking = yesgammuloc =[gammu2]port = /dev/ttyUSB5model = atconnection = atsynchronizetime = yeslogfile =logformat = nothinguse_locking = yesgammuloc =

Краткое описание настроек:

  • model тип модема и как gammu следует общаться с модемом, at с помощью AT-команд
  • connection = at тип соединения. Подключаемся на скорости 9600
  • use_locking говорит gammu, что нужно блокировать доступ к модему на время работы с ним. Иначе возможны различные ошибки и сбои


Вид настроек в терминале

Проверка работы


Gammu настроена и теперь мы можем протестировать отправку сообщений.

Базовая команда для отправки сообщения на английском и всего с одного модема выглядит следующим образом:

gammu sendsms TEXT +70001234567 -text "Test message"



Теперь рассмотрим варианты поинтереснее с использованием дополнительных аргументов

Если в системе несколько модемов, то добавляем нужный порт аргументом "-s <номер_порта>". Мы затрагивали этот момент, когда заполняли настройки. Нумерация начинается с 0 и в нашем случае это промежуток 0-2.

gammu -s 1 sendsms TEXT +70001234567 -text "Test message"

Отправка сообщений в PDU-формате (кириллица, прочие языки и спецсимволы) "-unicode"

gammu -s 1 sendsms TEXT +70001234567 -unicode -text "Тестовое сообщение"

Отправка сообщений в PDU-формате с автоматической разбивкой на несколько сообщений и последующей склейкой на телефоне "-autolen 5". Данный аргумент подразумевает использование числа, хотя объективно оно ни на что не влияет, по крайней мере у меня работает одинаково при любом числе, поэтому я решил, что сообщения длиной больше 5 у меня не будет и использую это число

gammu -s 1 sendsms TEXT +70001234567 -unicode -autolen 5 -text "Тестовое сообщение"

Есть очень интересный аргумент, который полезен, если вы отправляете одноразовые коды "-replacemessages 1". Суть его работы заключается в следующем мы указываем телефону, что при получении сообщения он должен заменить сообщений с ID 1, если оно имеется в полученных на то, что поступило сейчас. Таким образом телефон автоматически затирает все ранее полученные сообщения и в переписке всегда отображается только последнее полученное. Единственное, решить стоит его использовать или нет, желательно в самом начале, чтобы всегда заменять ID 1

gammu -s 1 sendsms TEXT +70001234567 -unicode -autolen 5 -flash -replacemessages 1 -text "Тестовое сообщение"



А теперь, самый интересный, на мой взгляд аргумент "-flash".

Сообщение отправленное с этим параметром отображается телефоном сразу во всплывающем окне. Проигнорировать его невозможно, т.к требуется решение абонента о сохранении или отклонении данного сообщения. Но самое главное абонент не видит номер телефона отправителя в этом окне (помните это был один из минусов собственного шлюза). Таким образом, если мы используем шлюз для авторизации пользователей и отправки им кодов, то это решение позволяет сразу показать код без захода в программу Сообщения. По получении звукового сигнала о входящем сообщении человек берет телефон, снимает блокировку и сразу видит его на экране. Если абонент нажмет на Отмена, то сообщение не будет сохранено в памяти телефона. Отправляем сообщение и смотрим как оно выглядит:

gammu -s 1 sendsms TEXT +70001234567 -unicode -autolen 5 -flash -replacemessages 1 -text "Тестовое сообщение"


Всё!

В следующей статье


В этой части статьи мы настроили отправку сообщений, но оно происходит вручную. Во второй части статьи мы напишем небольшое API (на PHP) для получения запросов по http, сохранении их в базу данных и автоматической отправке через активный шлюз и не только

Подробнее..

Свой личный SMS-шлюз. Часть 2 создаём API и форму отправки

03.05.2021 12:05:25 | Автор: admin

Представляю вам вторую часть из серии статей по созданию своего шлюза.
В первой части мы настроили Gammu, рассмотрели особо интересные параметры и успешно произвели отправку SMS сообщения. Сейчас нам предстоит задача посложнее создать некую программную прослойку (API), для того, чтобы можно было работать со шлюзом путем отправки запросов на этот API. В первую очередь это комфорт, во вторую большое количество дополнительных возможностей.

Если вы не знакомы с первой частью, советую сначала ознакомиться с ней:
Свой личный SMS-шлюз. Часть 1 цели, задачи, сборка и тестирование

Постановка задачи


Первое, что нужно для себя понять, какими свойствами и возможностями должен обладать наш код, что мы хотим от него получить и как с ним взаимодействовать. Для этого я поставил для себя пару вопросов и постарался максимально на них ответить.

На чем будем писать backend
Тут все просто, что умеем, на том и пишем, поэтому в моем случае PHP

Авторизация
Конечно. Сервис будет смотреть в интернет, поэтому авторизация обязательна.

Один пользователь одна sim-карта?
Конечно нет. У нас сервис для личного пользования и мы хотим иметь один логин, но при этом отправлять с нескольких номеров. Но если появится необходимость выделить один шлюз под конкретный сервис, мы должны иметь возможность добавления пользователей.

Как мы хотим общаться с этим API, откуда будут попадать запросы
Общение будет через POST/GET. Запросы могут отправляться различными устройствами, в том числе и теми, которые не умеют POST или заморочно реализовать, поэтому принимать и обрабатывать будем $_REQUEST. Также мы хотим иметь возможность отправки сообщений через простую форму на сайте.

Один запрос один адресат?
Нет. В одном запросе с одним текстовым сообщением должна быть возможность указать несколько адресатов. Суть этого понятна. Например я отслеживаю наличие ЭЭ на даче и в случае отключения хочу получить уведомление на все свои телефоны, а может даже телефон супруги почему бы и нет, ведь уведомление важное.

История отправленных сообщений
Конечно, история это наша важная составляющая жизни, поэтому ее мы всегда храним

Балансировка нагрузки на карты
Да. Мы обладаем чувством меры и будем отправлять с одной карты не более какого-то числа сообщений, а значит их нужно считать и перед выбором шлюза проверять на исчерпание лимита.

Задача на разработку поставлена, цель ясна, итак приступим


Первое что мы сделаем, определим структуру база данных. Без нее, при наших потребностях никак. Использовать будем MySQL.
Дальше нужно будет написать пару классов, к которым мы были обращаться.

Приступим к созданию БД и создание таблиц
Я буду использовать несколько таблиц для:

  • users данные пользователей
  • smsc_gateway данных шлюзов
  • gateway_smscount счётчик отправленных сообщений по каждому шлюза в конкретный месяц
  • sms_queue очередь отправки сообщений
  • sms_archive история сообщений

Структура БД дамп таблиц
# Дамп таблицы users# ------------------------------------------------------------CREATE TABLE `users` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `uuid` varchar(10) NOT NULL,  `login` varchar(50) NOT NULL,  `password` varchar(32) NOT NULL,  `comment` varchar(200) NOT NULL,  `status` varchar(11) DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;# Дамп таблицы smsc_gateway# ------------------------------------------------------------CREATE TABLE `smsc_gateway` (  `id` int(20) NOT NULL AUTO_INCREMENT,  `gw_phone` varchar(11) NOT NULL DEFAULT '',  `uuid` varchar(10) NOT NULL,  `host` varchar(15) NOT NULL DEFAULT '',  `port` varchar(5) NOT NULL,  `password` varchar(12) DEFAULT '',  `maxcount` varchar(6) NOT NULL,  `status` int(1) NOT NULL,  `gateway_id` int(2) DEFAULT NULL,  `state` varchar(11) DEFAULT NULL,  `comment` varchar(100) NOT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;# Дамп таблицы gateway_smscount# ------------------------------------------------------------CREATE TABLE `gateway_smscount` (  `id` int(20) NOT NULL AUTO_INCREMENT,  `gw_phone` varchar(11) NOT NULL DEFAULT '',  `date` varchar(10) NOT NULL,  `count` int(6) NOT NULL DEFAULT '0',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;# Дамп таблицы sms_queue# ------------------------------------------------------------CREATE TABLE `sms_queue` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `uuid` varchar(11) NOT NULL DEFAULT '',  `dateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,  `status` varchar(10) DEFAULT NULL,  `data` varchar(500) NOT NULL DEFAULT '',  `phone` varchar(11) NOT NULL DEFAULT '',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;# Дамп таблицы sms_archive# ------------------------------------------------------------CREATE TABLE `sms_archive` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `gateway_id` varchar(11) DEFAULT NULL,  `dateTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP,  `data` varchar(500) DEFAULT NULL,  `status` varchar(11) DEFAULT NULL,  `phone` varchar(11) DEFAULT NULL,  `uuid` varchar(11) DEFAULT NULL,  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;



Стоит подробнее остановиться на таблице с данными шлюза и используемых полей smsc_gateway. Здесь мы используем:

  • uuid id пользователя, которому назначен данный шлюз
  • host ip-адрес компьютера с модемами для подключения по ssh
  • port порт мы тоже укажем, так как желательно не использовать стандартный 22-й, а также это позволит при необходимости использовать port forwarding
  • password пароль ssh
  • gw_phone фактической номер телефона
  • maxcount ограничение на количество отправляемых сообщений
  • status статус. 1 активен, 0 заблокирован.
  • gateway_id канал этого модема в Gammu (помните, у нас может быть несколько модемов)
  • state статус шлюза. lock заблокирован.
  • comment свободное поле с комментарием, чтобы просто делать заметки, если нужно

Классов будет всего 5:

  1. Авторизация пользователя Users_Auth.class.php
  2. Работа с PDO MYSQL_PDO.class.php
  3. Обработчик по работе с входящими данными SMS SMS_data_handle.class.php
  4. Работа с Gammu Gammu_SMS.class.php
  5. Возврат http ответов в json http_response.class.php

Дальше я буду объяснять как поток данных будет ходить по API опираясь на базу данных. Мне кажется так нагляднее и понятнее. Также я приведу куски этого кода под спойлерами.

В итоге мы получаем такую последовательность действий.

Пользователь отправляет запрос с параметрами:

.../smsc.php?login={user_name}&pass={user_password}&tel={phone_number}&msg={message}&flash=1&replacemessages_id=1

Значения flash и replacemessages мы рассматривали в прошлой статье. В {phone_number} можно указать несколько номеров телефонов через ",". + в номере телефона указывать не нужно, но обязательно указывать номер в международном формате (для России это 7). Так же в стоку можно добавить еще один параметр &attempts={число} количество попыток внутри одной отправки, то есть, если можем при отправке вернул ошибку, пытаться ли отправить тут же еще раз?

Вот, что происходит под капотом smsc.php

<?phprequire_once __DIR__.'/functions/config.php';XSS_secure();if (!Users_Auth::do($PDO)) http_response::return(401, ["description" => "User not found or login / password is incorrect"]);$sms_handle = new SMS_data_handle($PDO);$sms_handle->save();function XSS_secure() {     function replace($arr) {        $filter = array("<", ">");        $filter_replace = array("<", ">");         for ($i=0; $i < count($filter) ; $i++) {            $str = str_replace($filter[$i], $filter_replace[$i], $arr);        }    return $str;    }     if ($_GET) $_GET = replace($_GET);    if ($_POST) $_POST = replace($_POST); }?>

Первым делом мы подключаем файл с настройками config.php:

require_once __DIR__.'/functions/config.php';

Содержание файла:

<?php // ini_set('error_reporting', E_ALL);// ini_set('display_errors', 1);// ini_set('display_startup_errors', 1); spl_autoload_register(function ($class_name) {    require_once "classes/{$class_name}.class.php";}); $PDO_param = ['db_host' => '__', // database hostname or ip'db_name' => '__', // database name'db_user' => '__', // database username'db_pass' => '__', // databse user password'db_charset' => 'utf8','pdo_opt' => [        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_LAZY,        PDO::ATTR_EMULATE_PREPARES => false,        ]]; $PDO = new MYSQL_PDO($PDO_param); ?>

делаем небольшую проверку на XSS, а далее проверяем авторизацию, вызывая метод класса Users_Auth::do($PDO):

class Users_Auth{    static function do($PDO)    {      if (!@$_REQUEST['login'] || !@$_REQUEST['pass']) http_response::return(403, ['description' => 'Check your login and password']);     // Проверяем авторизацию      $user = $PDO->query("SELECT id, status FROM users WHERE login= ? AND password= ?", [$_REQUEST['login'], md5(trim($_REQUEST['pass']))]);      if ($user->rowCount() == 0)          return 0;          // http_response::return(401, ["description" => "User not found. Login and password is incorrect"]);      $row = $user->fetch();      if ($row->status != 'active')          return 0;          // http_response::return(401, ["description" => "User status: {$row->status}"]);      return 1;    }}

Если получили false авторизация не удалась, возвращаем код и описание ошибки в json, если необходимо.

Если авторизация успешная вызываем $sms_handle->save(), проверяем переданы ли обязательные параметры телефон и текст сообщения, проверяем в БД статус пользователя, разбираем строку запроса и приводим в нужный нам вид, удаляем пробелы и "+" из номера телефона, а также разделяем их по запятой. Таким образом получаем массив номеров телефонов и текста сообщения, которое нужно на них отправить. Делаем из этого json и сохраняем в таблицу очереди на отправку. Проверка на наличие телефона обязательна. Если попытаться отправлять сообщения без указания номера телефона, возникнет ошибка в Gammu, и шлюз будет занят на несколько секунд. Когда шлюз освободится возникнет аналогичная повторная ситуация, что в свою очередь создаст так называемую пробку и последующие сообщения из очереди просто не смогут уйти.

public function save() {if (empty($_REQUEST['tel'])) http_response::return(400, ["success" => false, "description" => "Phone number is empty"]);if (empty($_REQUEST['msg'])) http_response::return(400, ["success" => false, "description" => "Message is empty"]);$msg = $_REQUEST['msg'];$search = array(' ', '+');$replace = array('', '');$phone_array = explode(",", str_replace($search, $replace, $_REQUEST['tel']));$query = $this->PDO->query("SELECT uuid FROM users WHERE login = ?", [$_REQUEST['login']]);$user_uuid = $query->fetch();$data = [    'message' => $msg,    'flash' => @$_REQUEST['flash'],    'replacemessages_id' => @$_REQUEST['replacemessages_id'],    'attempts' => @$_REQUEST['attempts']];$data['attempts'] = (@$_REQUEST['attempts']) ?: 1;foreach ($phone_array as $phone) {    $data['phone'] = $phone;    $this->PDO->query("INSERT INTO sms_queue SET uuid= ?, data= ?, phone= ?", [$user_uuid->uuid, json_encode($data, JSON_UNESCAPED_UNICODE), $phone]);}http_response::return(200, ["success" => true, "description" => "Saved to queue"]);  }

Дальше мы используем простой скрипт, который поставим в cron и будем вызывать раз в 5-10 секунд (по вкусу) send_queue.php

<?php require_once __DIR__.'/config.php'; $sms_handle = new SMS_data_handle($PDO);$sms_handle->send(); /*add follow lines to cron  crontab -edon't forget to replace <full_path_to> to your really path* * * * * ( php /<full_path_to>/send_queue.php )* * * * * ( sleep 10 ; php /<full_path_to>/send_queue.php )* * * * * ( sleep 20 ; php /<full_path_to>/send_queue.php )* * * * * ( sleep 30 ; php /<full_path_to>/send_queue.php )* * * * * ( sleep 40 ; php /<full_path_to>/send_queue.php )* * * * * ( sleep 50 ; php /<full_path_to>/send_queue.php )*/?>

Он будет обращаться к методу класса обработчика сообщений SMS_data_handle->send(). Здесь уже начинается самое интересное.

Мы получаем сообщение за последние 10 минут без тегов статуса. Если нашли такое, ставим на него тег process и берём в работу.

Извлекаем из тела json uuid пользователя, обращаемся к таблице и получаем список активных шлюзов. Идем в таблицу со счётчиком и проверяем, не превышен ли лимит на отправку. Если мы получили активный шлюз и счётчик не превышен, ставим на него тег lock, чтобы никакой другой процесс уже не смог параллельно к нему обратиться. Все вызовы происходит внутри метода send(), но логика раскидана по другим методам класса. По указанному выше описанию работы метода эти обращения легко видны.

Далее мы создаем объект класса $send_proc = new Gammu_SMS($param) с параметрами и обращаемся к методу $send_proc->send($attr) с атрибутами

Весь код метода send():

public function send($с = 1) {     $sended_sms = 0;    for ($i = 0; $i < $с; $i++) {       $sms_queue = $this->get_sms_queue(1);      if (!$sms_queue->rowCount()) http_response::return(200, ["description" => "Nothing to do. Sent. count: {$sended_sms}"]);       $sms_count = $sms_queue->rowCount();      $msg_row = $sms_queue->fetch();       $this->PDO->query("UPDATE sms_queue SET status = ? WHERE id = ?", ["process", $msg_row->id]);       $user_gateway = ($this->get_gateway($msg_row->uuid));       if (!$user_gateway) {      $this->PDO->query("UPDATE sms_queue SET status = NULL WHERE id= ?", [$msg_row->id]);      http_response::return(403, ["description" => "Not active gateways or get limit of message count"]);      }       $this->gateway_lock($user_gateway->id);       $param = [      'host' => $user_gateway->host,      'port' => $user_gateway->port,      'login' => 'root',      'password' => $user_gateway->password,      ];       $sms_data = json_decode($msg_row->data);      $sms_data->message = $sms_data->message;       $attr = [      'phone' => $sms_data->phone,      'message' => $sms_data->message,      'attempts' => $sms_data->attempts,      'flash' => $sms_data->flash,      'replacemessages_id' => $sms_data->replacemessages_id,      'gateway' => $user_gateway->id,      ];      // sleep(5);      $send_proc = new Gammu_SMS($param);      if ($send_proc->send($attr)) {      $this->sms_2archive($msg_row->id, $user_gateway->id);      $this->update_gwcount($user_gateway->gw_phone);      $sended_sms++;       } else {      $this->PDO->query("UPDATE sms_queue SET status = NULL WHERE id= ?", [$msg_row->id]);      }     $this->gateway_release($user_gateway->id);     }     http_response::return(200, ["success" => true, "description" => "Message sent. Count: {$sended_sms}"]);   }

Если объект вернул true, то переносим сообщение в архив и увеличиваем счётчик отправленных сообщений. Иначе снимаем тег proccess и через некоторое время будет повторная попытка отправки по cron.

Особо внимательные заметили, что мы вызываем метод с дефолтным параметром равным одному send($с = 1). Параметр $c заложен на перспективу и позволяет нам, в случае необходимости получать пачку сообщений из базы данных и обрабатывать их отправку в цикле. Для этого в файле, вызываемом в cron нужно в вызове метода указать число сообщений для выборки их БД $sms_handle->send({число});

Обратим внимание еще на один момент. В файле smsc.php есть возможность отправлять сообщения сразу после того, как оно было добавлено в БД. Для этого нужно раскомментировать следующую строку:

// $sms_handle->send();

Это позволит нам отказаться от cron, но есть один нюанс желательно использовать этот метод, если вы отправляете сообщения только на один номер и запросы к шлюзу не могут быть чаще чем раз в 30 секунд. Иначе возможны ошибки связанные с наложением запросов и если шлюз занят, то сообщение не отправится.

Теперь наш шлюз работает через API и умеет отправлять сообщения.

Ну и бонусом мы сделаем простую форму для отправки сообщений с сайта. Ее код не нуждается в пояснении, она просто принимает от вас тест и отправляет POST-запрос на указанный нами скрипт. Единственное в блоке отправки ajax нужно заменить url: "/<*.php>" на адрес вашего скрипта smsc.php

Итак подведем итоги проделанной работы. Мы создали аппаратную платформу, научились отправлять сообщения через терминал и расширили возможности системы собственным API для легкого доступа к шлюзу устройств способных отправлять GET/POST-запросы. Хранить историю и балансировать нагрузку между картами и прочее. Все это сильно упрощает работы со шлюзом и позволяет хранить все в одном сервисе.
Внимание, я не претендую на великолепную красоту кода и буду рад любой объективной критике для понимания своих ошибок (в случае наличия) и совершенствования навыков.
Репозиторий данного проекта на Github

Подробнее..

Из песочницы Двухфакторая аутентификация VPNMikrotik просто и масштабируемо

15.06.2020 12:08:41 | Автор: admin
Здравствуйте!

На написание данной статьи меня побудило прочтение аналогичного содержания статьи пользователя nkusnetsov. По количеству просмотров видно, что сообществу интересна данная тема.

Поэтому я решил поделиться с вами собственным решением, которое было ранее реализовано мной и в базовом виде обладает:

  • Низким уровнем вхождения и простотой кода (для понимания/отладки другим сотрудником)
  • Простые скрипты ROS не создают никакой нагрузки и работают даже на hAP Lite
  • Масштабируемость возможность подключения большого количества VPN-шлюзов с целью снижения нагрузки или географического распределения
  • Возможность использования Mikrotik CHR в качестве VPN-сервера
  • 1хN 1 SMS-шлюз на неограниченное количество роутеров с возможностью расширения при росте нагрузки
  • Возможность привязки отдельного роутера к конкретному модему (для чего? об этом позже)
  • Использование всего одного php скрипта на удаленном сервере
  • Не важно какое устройство инициировало VPN-соединение, авторизация по ссылке из SMS

При несложной доработке кода:

  • Возможность вести log авторизаций сотрудников на сервере (возможность реализована в расширенной версии, если есть интерес выложу)
  • Увеличить отказоустойчивость и снижение нагрузки системы путем отправки SMS рандомно с нескольких модемов


Постановка задачи и решение


Задача 1


В процессе разработки данного решения была поставлена задача минимизировать количество usb-модемов уменьшив стоимость владения, упростить администрирование, локализовать модемы в одном месте и тем самым улучшить ремонтопригодность системы.

Было решено использовать один роутер установленный на надежном канале как базовый SMS-шлюз. Единственное я не знал какова будет нагрузка на систему, справятся модемы и не заблокирует ли оператор. Поэтому я заложил возможность подключения нескольких модемов и дальнейшего разбиения системы на группы. Причем дополнительные модемы могут быть подключены к базовому шлюзу через простой usb-hub. Но для полноценного функционирования, в минимальном функционале, нужен один модем в не зависимости от количества роутеров.

Задача 2


Предусмотреть возможность использования локальных сим-карт в зоне установки роутера.
Пример: широкая филиальная сеть с несколькими магазинами в Казахстане. Отправка sms-сообщения из РФ будет стоить достаточно дорого. Данное решение позволяет сотрудникам из РК получать sms с локального номера.

Но в процессе размышлений оказалось, что решение уже было найдено и реализовано в Задаче 1.

Задача 3


Авторизация туннеля с мобильного устройства без необходимости физически находиться на авторизуемом устройстве.

Цель возможность авторизовывать не только пользовательские туннели, но и любые vpn-соединения: Mikrotik->Mikrotik, Сервер->Mikrotik и т.д При этом пользователю, ответственному за данные туннели, необходимо просто перейти по ссылке из SMS сообщения, в которой также отображается какой туннель хочет авторизоваться.

Для возможности авторизации таким способом появилась необходимость вынести скрипт на внешний ресурс сервер, доступный из любой точки а также код авторизации использовать для целей маркировки адресного листа в firewall и последующей разблокировки.

Решение данной задачи уже однозначно подразумевало использование RouterOS API (или SSH). Победу одержало API, как наиболее простой вариант.

Логика


  1. Пользователь авторизуется с заранее добавленным логином и паролем
  2. VPN-шлюз заносит его ip в адресный лист ожидания и отправляет POST запрос на сервер
  3. Сервер получает данные, проверяет, формирует код-авторизации и отправляет на контактный номер SMS вида: To autorize user 79001112233 connection open http_://synome.ru/?ruid=vrG7yYMbZ6&auth=YU6zc
  4. Пользователь переходит по ссылке с любого устройства
  5. Сервер делает запрос на роутер, проверяет наличие в адрес-листе записи с кодом авторизации в комментарии. Если запись найдена, удаляет ее, а пользователю отображает страницу удачной авторизации

Во всех остальных случаях, если что-то не прошло проверку пользователь получит Request error.

Настройка и код


Теперь перейдем от идейной части к настройке Mikrotik, коду и их описанию

Добавляем на Mikrotik:


Firewall


Обязательно создаем список разрешенных ресурсов, среди которых: собственный адрес MT, любой публичный DNS, адрес сервера на котором будет происходить авторизация
/ip firewall address-listadd address=10.10.0.1 list=Allow-listadd address=8.8.8.8 list=Allow-listadd address=synome.ru list=Allow-list


Добавляем правило блокирующее трафик VPN-пользователей из списка ожидания на любой хост кроме разрешенных
/ip firewall rawadd action=drop chain=prerouting dst-address-list=!Allow-list src-address-list=VPN-blocked disabled=no


PPP Profile


Создаем профиль в котором устанавливаем idle-таймаут соединения. Время должно быть меньше чем указанное в следующем скрипте On Up, в противном случае, если авторизация не была произведена, то после удаления списка из address-list пользователь получит доступ на время равное разнице (idle-таймаут минус address-list timeout)

Добавляем профиль
/ppp profileadd dns-server=10.10.0.1 idle-timeout=59m local-address=10.10.1.100 name=2F-VPN use-compression=no use-encryption=no use-mpls=no


Далее на последней вкладке Script добавляем:

On Up
:global pass "19RuOU89";:global ruid "vrG7yYMbZ6";:local userip [/ppp active get [find name=$user] address];# if phone number stored in comment#:local userphone [/ppp secret get [find name=$user] comment];# if phone number = username:local userphone $user;:local authkey [/tool fetch http-method=post http-data="ruid=$ruid&pass=$pass&tel=$userphone" url="http://personeltest.ru/away/synome.ru/" mode=http as-value output=user];/ip firewall address-list remove [find address=$userip];/ip firewall address-list add address=$userip list=VPN-blocked timeout=1h comment=($authkey->"data");:log info message="User connect:";:log info message=$userphone;:log info message=$userip;:log info message=($authkey->"data");


On Down
:local userip [/ppp secret get [find name=$user] remote-address];/ip firewall address-list remove [find address=$userip];:log info message="User disconnect:";:log info message=$user;:log info message=$userip;


Смысл скриптов

При подключении: в самом начале мы задаем логин и пароль роутера, которые будут проверяться на сервере. При авторизации пользователя получаем его номер телефона (может быть в имени или в комментарии) и локальный ip-адрес. Отправляем POST-запрос на сервер и получаем в ответе код авторизации. Добавляем ip-адрес в address-list VPN-blocked с кодом авторизации в комментарии и тайм-аутом на 1 минуту больше чем в профиле. Выводим все в лог.

При отключении: получаем ip-адрес пользователя, находим его в address-list и удаляем. Все выводим в лог.


PPP Secrets


Добавляем пользователя
/ppp secretadd comment="70001112233" name=70001112233 password=testuser profile=2F-VPN remote-address=10.10.1.100


Номер телефона можно указать прямо в name, но если хотим иметь возможность задавать один номер на несколько аккаунтов (для авторизации нескольких туннелей), то номер указываем в комментарии, при этом в скрипте On Up нужно изменить закомментированность строк

Изменение (вторую открываем, четвертую закрываем)
# if phone number stored in comment:local userphone [/ppp secret get [find name=$user] comment];# if phone number = username#:local userphone $user;


Ну и самое главное включаем PPTP или L2TP сервер.

На этом с Mikrotik работа закончена.

Серверная часть на PHP


Ниже приведен код. Он достаточно подробно комментирован, поэтому не буду писать лишний текст. Самое главное заменить данные в $host и $ruid_data на свои.

index.php
<?php// ------------------------------------------------------------------------------//  Copyright (с) 2020//  Author: Dmitri Agababaev, d.agababaev@duncat.net////  Copyright by authors for used RouterOS PHP API class in the source code files////  Redistributions and use of source code, with or without modification, are//  permitted that retain the above copyright notice// ------------------------------------------------------------------------------require_once('routeros_api.class.php');// Адрес по которому доступен данный скрипт$host = 'http://synome.ru/';// МАССИВ ДАННХ ВСЕХ РОУТЕРОВ, А ТАКЖЕ РОУТЕРА ЯВЛЯЮЩЕГОСЯ SMS-ШЛЮЗОМ$ruid_data = array(    // роутеры учавствующие в авторизации    // пароль в md5 , глобальный ip-адрес, логин входа на роутер, пароль, SMS-шлюз через который происходит отправка SMS    'vrG7yYMbZ6' => array('mdpass' => '5568ba82f332494d9ff8754b51e7b28a', 'ip' => '10.10.0.1', 'login' => 'user_vpn', 'password' => 'kji&@11az', 'smsgw' => 'SMS_gw1'),    // SMS-шлюзы    // ip-адрес шлюза (глобальный или локальный если в одной сети с сервером), логин, пароль, порт USB-модема, канал USB-модема    'SMS_gw1' => array('ip' => '172.16.1.3', 'login' => 'sms2F', 'password' => 'skIU8w!0', 'port' => 'usb1', 'channel' => '0'));// ВХОДНЕ ПРОВЕРКИ ЗАПРОСОВif (!$_REQUEST) die('Request error'); // если запроса нет  сбросif (!$_REQUEST['ruid']) die('Request error'); // если не указан ruid - сбросif (!array_key_exists($_REQUEST['ruid'], $ruid_data)) die('Request error'); // если роутер не существует  сбросif ($_REQUEST['auth']) autorize(); // если запрос на авторизацию, то пускаем без пароля и проверяем авторизациюif (!ruid_auth()) die('Request error'); // проверяем пароль роутера для отправки SMSif ($_REQUEST['tel']) send_authcode(); // если задан номер телефона, отправляем SMS// ПРОВЕРКА НА НАЛИЧИЕ РОУТЕРА В СПИСКЕ РАЗРЕШЕННХ и пароля авторизацииfunction ruid_auth() {  global $ruid_data;  if (!$_REQUEST['pass']) return false; // если пароль не задан  сброс  // проверяем md5-хэш пароля  if (md5($_REQUEST['pass']) == $ruid_data[$_REQUEST['ruid']]['mdpass']) return true;  return false;}// ФУНКЦИЯ ОТПРАВКИ ССЛКИ С КОДОМ АВТОРИЗАЦИИ ЧЕРЕЗ ROS APIfunction send_authcode() {  global $ruid_data;  global $host;  $sms_gw = $ruid_data[$_REQUEST['ruid']]['smsgw']; // данные sms-шлюза  // генерируем код авторизации  $auth_code = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789'), 0, 5);  // подключаем класс  $API = new RouterosAPI();  // Формируем сообщение отправляемое пользователю  только eng или транслит  $message = 'To autorize user '.$_REQUEST['tel'].' connection open  '.$host.'?ruid='.$_REQUEST['ruid'].'&auth='.$auth_code;  // если подключились отправляем SMS  if ($API->connect($ruid_data[$sms_gw]['ip'], $ruid_data[$sms_gw]['login'], $ruid_data[$sms_gw]['password'])) {      // Команда отправки SMS      $ARRAY = $API->comm("/tool/sms/send", array(      "port"=>$ruid_data[$sms_gw]['port'],      "channel"=>$ruid_data[$sms_gw]['channel'],      "phone-number"=>$_REQUEST['tel'],      "message"=>"To autorize user ".$_REQUEST['tel']." connection open  ".$host."?ruid=".$_REQUEST['ruid']."&auth=".$auth_code,));      // если отправка не удалась и получили ошибку модема, то выполняем сброс питания usb для перезагрузки модема      if($ARRAY['!trap']) {        $API->comm("/system/routerboard/usb/power-reset");        die('Stop with error: '.$ARRAY['!trap'][0]['message'].' Making power reset of usb-port');}  }  $API->disconnect();  die($auth_code);}function autorize() {  global $ruid_data;  // подключаем класс  $API = new RouterosAPI();  if ($API->connect($ruid_data[$_REQUEST['ruid']]['ip'], $ruid_data[$_REQUEST['ruid']]['login'], $ruid_data[$_REQUEST['ruid']]['password'])) {    // если подключились отправляем команду    $API->write('/ip/firewall/address-list/print', false);    $API->write('?comment='.$_REQUEST['auth'], false);    $API->write('=.proplist=.id');    // получаем ответ    $ARRAYS = $API->read();    // ЕСЛИ ЗАПИСЬ НЕ СУЩЕСТВУЕТ В АДРЕС-ЛИСТЕ - СБРОС    if (!$ARRAYS[0]) die('Request error');    // удаляем запись    $API->write('/ip/firewall/address-list/remove', false);    $API->write('=.id=' . $ARRAYS[0]['.id']);    $READ = $API->read();  }  $API->disconnect();  // ИНФОРМИРУЕМ ПОЛЬЗОВАТЕЛЯ ОБ УСПЕШНОЙ АВТОРИЗАЦИИ  die('      <html>      <body style="background-color: #282c34; color: #fff; height: 100vh; display: flex;">        <div style="margin: auto; max-width: 50%;">          <p style="font-size: 24pt; font-weight: bold; margin: -300px 0 50px;">            VPN-соединение установлено и авторизовано, можете продолжить работу          </p>          <p style="font-size: 14pt; color: #aaa;">             В случае недоступности сервисов обратитесь к вашему системному администратору<br/>          </p>        </div>      </body>      </html>');}?>


RouterOS API class PHP используемый в коде можно взять на GitHub.

Благодарю за внимание. Буду рад любым комментариям.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru