Минимальные примеры на нашем сайте написаны так, чтобы любой клиент, даже далекий от web программирования, мог взять куски кода и сделать свой продукт. Но бездумное копирование кода может привести к финансовым потерям. Яркий пример — минимальный код внедрения кнопки Click to Call.
Click to Call («звонок по клику») — кнопка на сайте, которая позволяет пользователям совершить звонок по заранее заданному номеру прямо из браузера. Очень удобный маркетинговый инструмент, который повышает конверсии и прибыль.
Если внедрять кнопку Click to Call по методике описанной здесь, злоумышленник может подменить номер вызываемого абонента или использовать учетные данные вашего SIP сервера, чтобы звонить пингвинам в Антарктиду и пересказывать им «Войну и Мир». И тогда прибыль от внедрения кнопки Click to call может превратиться в огромные убытки.
Передавать параметры подключения к SIP серверу в JS коде небезопасно, поэтому для Production реализации мы рекомендуем хранить параметры подключения на стороне сервера и подставлять их при инициализации звонка. Для этого можно использовать так называемые REST Hook скрипты.
Поймали на крючок
REST Hook — это простые скрипты, которые работают с JSON в теле HTTP-запроса и отдают JSON в теле HTTP ответов. Скрипты REST Hook подменяют собой стандартные приложения WCS API и позволяют обрабатывать данные о коннектах, звонках и видеопотоках на бэкенд сервере.
REST Hook могут быть использованы для следующих целей:
- аутентификация коннектов к серверу по токену или по паролю;
- получение в реальном времени информации о коннектах, дисконнектах, начале и завершении потоков, звонков, и т.д.;
- переопределение данных, переданных с клиента. Например, можно переопределить и скрыть реальное имя потока или направление звонка;
- реализация кастомного сигналинга с передачей данных через WebSockets, например рассылка текстового сообщения в чате всем подключенным клиентам.
В этой статье рассмотрим, как можно безопасно передать учетные данные SIP сервера и номер вызываемого абонента для кнопки «Click to Call» с использованием технологии REST Hook.
Архитектура решения
- Фронтенд Web сервер — организует интерфейс с пользователем. Отображает web страницу с кнопкой «Click to Call».
- WCS — посредник между пользователем и SIP сервером. Конвертирует WebRTC поток от браузера в SIP формат.
- Бэкенд сервер — Web сервер, который обеспечивает работу REST Hook.
- SIP сервер и SIP телефон.
Логически мы разделяем фронтенд, бэкенд и WCS серверы, но физически их можно разместить на одной машине. В этой статье, для упрощения разбора примера, мы используем три отдельные виртуальные машины.
Реализация
Начнем настройку с бэкенд сервера.
Устанавливаем и конфигурируем Nginx и PHP, например Nginx на CentOS 7.
После чего, в файле /etc/nginx/nginx.conf в секции «server» добавляем следующие строки. Эта настройка обеспечит доступность нашего REST Hook скрипта для событий «/connect» и «/call»:
location / { try_files $uri $uri/ /index.php?$request_uri; }
В каталоге для файлов web сервера (у нас /var/www ) создаем файл «index.php», в нем размещаем основной код создаваемого REST Hook. Этот скрипт будет реализовывать проверку доступа по домену и передавать параметры подключения к SIP серверу и номер вызываемого абонента:
<?php $api_method = array_pop(explode("/", $_SERVER['REQUEST_URI'])); $incoming_data = json_decode(file_get_contents('php://input'), true); $domain = "yourdomain.com"; switch($api_method) { case"connect": $origin = $incoming_data['origin']; //logs error_log("sessionId: " . $incoming_data['sessionId']); error_log("origin: " . $origin); $found = strpos($origin, $domain); if ($found !== false){ error_log("User authorized by domain " . $domain); }else{ error_log("User not authorized by domain: " . $domain . " Connection failed with 403 status."); ubnormalResponse(403); } $rest_client_config = json_decode(file_get_contents('rest_client_config.json'), true); $incoming_data['restClientConfig'] = $rest_client_config; $incoming_data['sipLogin'] = "10001"; $incoming_data['sipAuthenticationName'] = "10001"; $incoming_data['sipPassword'] = "Password_123"; $incoming_data['sipDomain'] = "172.16.30.156"; $incoming_data['sipOutboundProxy'] = "172.16.30.156"; $incoming_data['sipPort'] = "5060"; break; case "call": // Callee Number $incoming_data['callee'] = "10002"; break; } header('Content-Type: application/json'); echo json_encode($incoming_data); function ubnormalResponse($code) { if ($code == 403) { header('HTTP/1.1 403 Forbidden', true, $code); } else { header(':', true, $code); } die(); } ?>
В том же каталоге /var/www создаем файл «rest_client_config.json». Исходный код можно найти в конце этой страницы.
В файле «rest_client_config.json» правим секцию «call» . Здесь указываем политику REST метода — перезаписывать данные и какое значение будет перезаписано с помощью скрипта:
"call" : { "clientExclude" : "", "restExclude" : "", "restOnError" : "LOG", "restPolicy" : "OVERWRITE", "restOverwrite" : "callee" },
Затем, переходим к WCS серверу. Предполагаем, что у вас уже есть установленный и настроенный экземпляр WCS. Если нет, то устанавливаем по этой инструкции. WCS можно запустить как виртуальный инстанс на Amazon, Google Cloud и DigitalOcean, или как контейнер в Docker.
В консоли вашего сервера с WCS проверяем доступность скрипта REST Hook для событий /connect и /call с помощью утилиты «Curl»:
curl http://172.16.30.123/connect curl http://172.16.30.123/call
замените 172.16.30.123 на IP адрес или доменное имя вашего бэкенд web сервера.
Если вывод утилиты Curl содержит нужную информацию — параметры для SIP подключения и номер вызываемого абонента — значит REST Hook мы настроили правильно.
Заходим в CLI WCS сервера:
ssh -p 2001 admin@localhost
в этой команде ничего менять не нужно, пароль по умолчанию: admin
и меняем приложение по умолчанию для обработки событий «/connect» и «/call» на наш новый REST Hook скрипт с помощью команды:
update app -l http://172.16.30.123/ defaultApp
замените 172.16.30.123 на IP адрес или доменное имя вашего бэкенд web сервера.
После всех этих настроек переходим к фронтенд web серверу.
Создаем на фронтенде два пустых файла Click-to-Call-min.html и Click-to-Call-min.js. Эти файлы будут содержать минимальный код для реализации кнопки «Click to call».
HTML код:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <script type="text/javascript" src="https://flashphoner.com/downloads/builds/flashphoner_client/wcs_api-2.0/current/flashphoner.js"></script> <script type="text/javascript" src="Click-to-Call-min.js"></script> </head> <body onload="init_page()"> <input type="button" id="callBtn" type="button" Value="Call"/> <div id="remoteAudio"></div> <div id="localAudio"></div> </body> </html>
Из JS кода минимального примера убираем данные для подключения к SIP серверу и номер вызываемого абонента.
JS код:
var SESSION_STATUS = Flashphoner.constants.SESSION_STATUS; var CALL_STATUS = Flashphoner.constants.CALL_STATUS; var localAudio; var remoteAudio; function init_page(){ Flashphoner.init({}); localAudio = document.getElementById("localAudio"); remoteAudio = document.getElementById("remoteAudio"); connect(); } function connect() { var url = "wss://172.16.30.124:8443" var sipOptions = { registerRequired: true }; var connectionOptions = { urlServer: url, sipOptions: sipOptions }; console.log("Create new session with url " + url); Flashphoner.createSession(connectionOptions).on(SESSION_STATUS.ESTABLISHED, function (session) { console.log(SESSION_STATUS.ESTABLISHED); }).on(SESSION_STATUS.REGISTERED, function (session) { console.log(SESSION_STATUS.REGISTERED); }); callBtn.onclick = call } function call(session) { var constraints = { audio: true, video: false }; var session = Flashphoner.getSessions()[0]; var outCall = session.createCall({ remoteVideoDisplay: remoteAudio, localVideoDisplay: localAudio, constraints: constraints, stripCodecs: "SILK" }); outCall.call(); callBtn.value = "Hangup"; callBtn.onclick = function () { callBtn.value = "Call"; outCall.hangup(); connect(); } }
Тестирование
Для тестирования нам понадобятся:
- тестовый стенд, который мы собрали выше (фронтeнд, WCS, бэкенд);
- SIP сервер;
- два SIP аккаунта;
- браузер;
- программный SIP телефон
Данные для подключения SIP аккаунта 10001 мы передаем в JS коде страницы с кнопкой «Click to Call» при помощи REST Hook. Кнопка «Click to Call» запрограммирована сделать вызов на номер 10002. Учетные данные для аккаунта 10002 внесем в программный SIP телефон.
Открываем созданную на фронтенд web сервере HTML страницу и нажимаем кнопку «Call»:
Принимаем входящий звонок на программном SIP телефоне и проверяем, что происходит обмен аудио потоками между абонентами:
Пришлось немного поработать, но результат того стоит. Теперь учетные данные SIP сервера и номер вызываемого абонента защищены от злоумышленников, и можно не переживать, что кто-то использует вашу телефонию в своих интересах.