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

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


Представляю вам вторую часть из серии статей по созданию своего шлюза.
В первой части мы настроили 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

Источник: habr.com
К списку статей
Опубликовано: 03.05.2021 12:05:25
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Php

It-инфраструктура

Api

Разработка под linux

Sms

Смс

Смс-рассылки

Смс-сервер

Смс-оповещение

Смс-аутентификация

Смс-шлюз

Sms-гейт

Sms-шлюз

Отправка cмс

Ruvds_статьи

Категории

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

  • Имя: Макс
    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