Минимальные примеры на нашем сайте написаны так, чтобы любой клиент, даже далекий от 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»:
1 2 3 | location / { try_files $uri $uri/ /index .php?$request_uri; } |
В каталоге для файлов web сервера (у нас /var/www ) создаем файл «index.php», в нем размещаем основной код создаваемого REST Hook. Этот скрипт будет реализовывать проверку доступа по домену и передавать параметры подключения к SIP серверу и номер вызываемого абонента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | <?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 метода — перезаписывать данные и какое значение будет перезаписано с помощью скрипта:
1 2 3 4 5 6 7 | "call" : { "clientExclude" : "" , "restExclude" : "" , "restOnError" : "LOG" , "restPolicy" : "OVERWRITE" , "restOverwrite" : "callee" }, |
Затем, переходим к WCS серверу. Предполагаем, что у вас уже есть установленный и настроенный экземпляр WCS. Если нет, то устанавливаем по этой инструкции. WCS можно запустить как виртуальный инстанс на Amazon, Google Cloud и DigitalOcean, или как контейнер в Docker.
В консоли вашего сервера с WCS проверяем доступность скрипта REST Hook для событий /connect и /call с помощью утилиты «Curl»:
1 2 | 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 сервера:
1 | ssh -p 2001 admin@localhost |
в этой команде ничего менять не нужно, пароль по умолчанию: admin
и меняем приложение по умолчанию для обработки событий «/connect» и «/call» на наш новый REST Hook скрипт с помощью команды:
1 | 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 код:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <!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 код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | 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 сервера и номер вызываемого абонента защищены от злоумышленников, и можно не переживать, что кто-то использует вашу телефонию в своих интересах.