Компания, в которой мне довелось работать, занимается продажей услуг по интернету. Каждое утро дежурная смена разбирает общий стек накопившихся заявок и начинается обзвон клиентов для уточнения заказов. В течение дня операторы еще и принимают входящие звонки. До начала моей затеи они использовали для звонков такой десктопный SIP-клиент:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/58e/3c6/a09/58e3c6a09f97951c4a98ea5bc6539b1f.png)
Эта звонилка устанавливалась на компьютере каждого сотрудника, принимала звонки и звонила, куда надо. Чтобы сделать какие-либо изменения в настройках, нужно было обойти все машины и сделать все вручную. При этом, если сотрудник работает удаленно, приходилось его консультировать по телефону, как это все сделать. И часто это было довольно непросто.
Но самым главным траблом было отсутствие интеграции с нашей web-системой и базой данных. Такие, казалось бы простые задачи, как открытие карточки клиента на входящий звонок, сохранение статистики звонков для каждого из сотрудников и мониторинг их активности из административного web интерфейса — все это очень сложно сделать с десктопным софтфоном, даже в том случае он имеет соответствующие возможности интеграции с браузером, например с помощью плагинов.
Возникла идея объединить в одной системе и базе данных всю внутреннюю работу и звонки. Я долго допиливал нашу CRM с функцией встроенной звонилки c записью разговоров.
Для реализации звонков рассмотрел ряд технологий и пришел к выводу, что их не так уж и много. Нашлась пара опенсорсных и коммерческих реализаций, а так же несколько SAAS сервисов, которые не подходили в силу внутренних политик безопасности — обрабатывать звонки через собственный сервер.
В начале пытался использовать sipml5:
Документацию пришлось собирать по кускам из сети. В результате получил более-менее рабочий телефон с SIP стэком на стороне браузера:
Установка, тестирование и настройки длились около 2 недель, в результате нашел ряд мелких, но неприятных багов, которые так и не удалось обойти, например один из них был связан с настройками Websockets через SSL. А после выхода Chrome браузера 35 версии web телефон отказался работать совсем.
Кроме этого мне не хотелось раскрывать SIP аккаунты операторам, а SIP стэк на стороне браузера предполагает их открытое использование и отправку через Websockets. Даже если Websockets работают через SSL, у потенциального злоумышленника есть возможность отдебажить js код и вытащить SIP пароль. Был вариант — делегировать SIP Digest аутентификацию нашему Web серверу, но до его реализации добраться так и не удалось.
Так выглядят SIP запросы на стороне браузера в отладочной консоли:
Полный доступ к SIP стэку из Javascript иметь в общем не плохо. В этом есть свои преимущества, например можно попытаться поправить какой-нибудь интеграционный баг в JS SIP сигналинге. Но здесь есть один нюанс. Чуть более чем 90% SIP вендоров не поддерживают в настоящий момент спецификацию The WebSocket Protocol as a Transport for the Session Initiation Protocol (SIP) RFC 7118 датируемую январем 2014 года по которой работает JS SIP, а это означает что webrtc2sip модуль должен работать как stateful SIP прокси и фактически дублировать поддержку SIP стэка на стороне сервера. Такой расклад показался сильно сложным для дальнейшей работы и поддержки и я решил уйти от SIP стэка на стороне браузера и найти какое-нибудь более простое и понятное API для таких задач с серверной частью, которую можно было бы хостить у себя.
В результате начал тестировать Web Call Server. Это не SAAS и позволяет обрабатывать звонки через свой сервер, что в данном случае и требовалось:
По функциям примерно то же самое что и у sipml5, те же WebRTC звонки на SIP и обратно. Есть еще поддержка Flash, но в ней не было необходимости, так как все операторы используют в основном Chrome и Firefox браузер, а тем, кто использует IE, пришлось пересесть на более “правильные” браузеры.
В нагрузку дается софтфон на JS с открытым исходным кодом, который можно перерисовать и адаптировать для web-страницы.
Основное отличие от sipml5 — это взаимодействие с сервером через API, а не через SIP over Websockets. Т.е. SIP стэка на стороне браузера нет. Он располагается только на стороне сервера. Это немного облегчило задачу front-end разработчику, т.к. SIP стэк на стороне браузера повергал его в смятение, а при работе с Javascript API и CSS стало возможным сосредоточиться на интерфейсной части.
Итак, как я все это внедрял.
1. Взял вот такой сервер на Amazon EC2:
Памяти и дискового пространства много не требуется. Разве что для логов. А вычислительные мощности CPU в таких задачах могут быть важны, поэтому взял не самый слабый инстанс.
2. Поднял Apache для web-интерфейса, установил и запустил WCS сервер.
3. На странице хрома появился стандартный web-телефон, код которого находится на github.
Интерфейс телефона мне не очень понравился, сразу же решил его редизайнить, а отладочная консоль справа оказалась вполне полезной. Жаль, что позже ее пришлось убрать, чтобы не пугать нормального пользователя.
4. Протестировал web-телефон на способность звонить. Использовал для этого наши прежние SIP-аккаунты. Все работает, как надо. И на мобильники звонит, и на SIP-телефоны, и удерживание звонков и трансфер, и блэкджек и…
Похожим образом работает созвон с мобильным телефоном.
5. Адаптировал код web-телефона для своей web-CRM, перерисовал его дизайн и теперь он выглядит так:
На адаптации стоит остановиться подробнее, т.к. перерисовкой дизайна дело не ограничилось.
Первой же серьезной задачей стала автоматическая регистрация web-телефона на SIP сервере. В противном случае, оператору пришлось бы вводить SIP логин и пароль повторно, уже после того, как он ввел логин и пароль для CRM системы. Встал вопрос, как интегрировать.
Оказалось, что в API есть для этого специальная функция loginByToken:
Для того чтобы разобраться, как эта функция работает, пришлось хорошо постараться.
C помощью документации и примеров удалось выяснить, что все это работает примерно так:
1) При создании токена на стороне CRM используется алгоритм шифрования AES, которым зашифровывается строка, включающая SIP логин и пароль пользователя, а так же другую необходимую информацию.
Ключ шифрования известен только нашему серверу, где развернута CRM, а так же WCS серверу. Кроме того, срок действия токена задается специальным атрибутом expires для того, чтобы не было возможности им повторно воспользоваться.
Криптование токена происходит в AES CTR mode. Ниже пример c openssl, в котором происходит генерация шифрованного токена с передачей SIP пароля:
В результате получил что-то вроде:
Слева название нашего приложения “CRM”, а справа созданый ранее токен.
Вставляю этот токен в конфиг flashphoner.xml web-телефона в таком виде:
В этом случае процедура автоматической регистрации по токену начнется сразу же после перезагрузки страницы.
2 и 3) loginByToken и расшифровка.
На стороне сервера в конфиге прописаны ключи шифрования для AES:
Таким образом, когда приходит токен с префиксом “CRM:” для его расшифровки используется соответствующий ключ.
В результате расшифровки WCS сервер получает зашифрованную ранее строку:
и из этой XML строки берет все данные необходимые для SIP регистрации.
3) Как только сервер расшифровал данные, он посылает SIP REGISTER запрос на SIP и на 401 ответ отдает уже нормальную Digest аутентификацию с использованием расшифрованных на предыдущем шаге SIP логина и пароля.
В этом случае SIP логин и пароль знают только сама CRM и Web Call Server. На браузер эти данные в открытом виде не попадают.
Таким образом мне удалось внедрить телефон в страницу оператора, не заставляя его хранить два разных аккаунта — один для CRM другой для SIP, потому что это очень неудобно. Теперь сразу после загрузки страницы вызывается loginByToken и телефон переходит в состояние готовности.
Некоторые результаты внедрения браузерных звонков:
1. Звонки теперь делаются с сайта и принимаются на сайте, где все действия фиксируются в системе.
2. Стало возможным прослушивание записанных разговоров, что помогает разрешать конфликтные ситуации с клиентами и разногласия между сотрудниками. Это важно для нашего распределенного офиса.
3. Количество принятых звонков увеличилось примерно на 20%. Стало понятно, что операторы не всегда поднимали трубку при звонке клиента.
На текущий момент можно сказать, что все работает, как задумано. Проблемные ситуации удалось разрешить без серьезного погружения в SIP матчасть.
Из недостатков можно отметить невозможность установки под Windows. Кстати, с установкой под Linux и интеграцией тоже пришлось повозиться и, похоже, что осилит ее только продвинутый пользователь/разработчик.
WebRTC аудио звонки работают стабильно и без каких-либо дополнительных браузерных плагинов, типа Flash Player. Так что можно сказать, что мне удалось реализовать задуманную интеграцию и две недели работы были потрачены не зря.
![](https://habrastorage.org/getpro/habr/post_images/58e/3c6/a09/58e3c6a09f97951c4a98ea5bc6539b1f.png)
Эта звонилка устанавливалась на компьютере каждого сотрудника, принимала звонки и звонила, куда надо. Чтобы сделать какие-либо изменения в настройках, нужно было обойти все машины и сделать все вручную. При этом, если сотрудник работает удаленно, приходилось его консультировать по телефону, как это все сделать. И часто это было довольно непросто.
Но самым главным траблом было отсутствие интеграции с нашей web-системой и базой данных. Такие, казалось бы простые задачи, как открытие карточки клиента на входящий звонок, сохранение статистики звонков для каждого из сотрудников и мониторинг их активности из административного web интерфейса — все это очень сложно сделать с десктопным софтфоном, даже в том случае он имеет соответствующие возможности интеграции с браузером, например с помощью плагинов.
Возникла идея объединить в одной системе и базе данных всю внутреннюю работу и звонки. Я долго допиливал нашу CRM с функцией встроенной звонилки c записью разговоров.
Для реализации звонков рассмотрел ряд технологий и пришел к выводу, что их не так уж и много. Нашлась пара опенсорсных и коммерческих реализаций, а так же несколько SAAS сервисов, которые не подходили в силу внутренних политик безопасности — обрабатывать звонки через собственный сервер.
В начале пытался использовать sipml5:
![image](https://habrastorage.org/getpro/habr/post_images/015/0d2/ff6/0150d2ff603c2a34dd79f15dbf6be9a3.png)
Документацию пришлось собирать по кускам из сети. В результате получил более-менее рабочий телефон с SIP стэком на стороне браузера:
![image](https://habrastorage.org/getpro/habr/post_images/c61/117/06f/c6111706f85e234415904c36103ffbcd.png)
Установка, тестирование и настройки длились около 2 недель, в результате нашел ряд мелких, но неприятных багов, которые так и не удалось обойти, например один из них был связан с настройками Websockets через SSL. А после выхода Chrome браузера 35 версии web телефон отказался работать совсем.
Кроме этого мне не хотелось раскрывать SIP аккаунты операторам, а SIP стэк на стороне браузера предполагает их открытое использование и отправку через Websockets. Даже если Websockets работают через SSL, у потенциального злоумышленника есть возможность отдебажить js код и вытащить SIP пароль. Был вариант — делегировать SIP Digest аутентификацию нашему Web серверу, но до его реализации добраться так и не удалось.
Так выглядят SIP запросы на стороне браузера в отладочной консоли:
![image](https://habrastorage.org/getpro/habr/post_images/0b4/edc/8f2/0b4edc8f2445eb3a0a77bb14f0dae4df.png)
Полный доступ к SIP стэку из Javascript иметь в общем не плохо. В этом есть свои преимущества, например можно попытаться поправить какой-нибудь интеграционный баг в JS SIP сигналинге. Но здесь есть один нюанс. Чуть более чем 90% SIP вендоров не поддерживают в настоящий момент спецификацию The WebSocket Protocol as a Transport for the Session Initiation Protocol (SIP) RFC 7118 датируемую январем 2014 года по которой работает JS SIP, а это означает что webrtc2sip модуль должен работать как stateful SIP прокси и фактически дублировать поддержку SIP стэка на стороне сервера. Такой расклад показался сильно сложным для дальнейшей работы и поддержки и я решил уйти от SIP стэка на стороне браузера и найти какое-нибудь более простое и понятное API для таких задач с серверной частью, которую можно было бы хостить у себя.
В результате начал тестировать Web Call Server. Это не SAAS и позволяет обрабатывать звонки через свой сервер, что в данном случае и требовалось:
![image](https://habrastorage.org/getpro/habr/post_images/f2b/c28/2d7/f2bc282d70d10669cf515fe86dc00e08.png)
По функциям примерно то же самое что и у sipml5, те же WebRTC звонки на SIP и обратно. Есть еще поддержка Flash, но в ней не было необходимости, так как все операторы используют в основном Chrome и Firefox браузер, а тем, кто использует IE, пришлось пересесть на более “правильные” браузеры.
В нагрузку дается софтфон на JS с открытым исходным кодом, который можно перерисовать и адаптировать для web-страницы.
Основное отличие от sipml5 — это взаимодействие с сервером через API, а не через SIP over Websockets. Т.е. SIP стэка на стороне браузера нет. Он располагается только на стороне сервера. Это немного облегчило задачу front-end разработчику, т.к. SIP стэк на стороне браузера повергал его в смятение, а при работе с Javascript API и CSS стало возможным сосредоточиться на интерфейсной части.
![image](https://habrastorage.org/getpro/habr/post_images/f69/0a6/2ab/f690a62abac808879b609c96de37b552.png)
Итак, как я все это внедрял.
1. Взял вот такой сервер на Amazon EC2:
Памяти и дискового пространства много не требуется. Разве что для логов. А вычислительные мощности CPU в таких задачах могут быть важны, поэтому взял не самый слабый инстанс.
![image](https://habrastorage.org/getpro/habr/post_images/5be/5e4/35e/5be5e435e29d27a8d1acc37663775fb9.png)
2. Поднял Apache для web-интерфейса, установил и запустил WCS сервер.
![image](https://habrastorage.org/getpro/habr/post_images/43f/a5e/944/43fa5e944a9230a6c13d166b00cf6329.png)
3. На странице хрома появился стандартный web-телефон, код которого находится на github.
Интерфейс телефона мне не очень понравился, сразу же решил его редизайнить, а отладочная консоль справа оказалась вполне полезной. Жаль, что позже ее пришлось убрать, чтобы не пугать нормального пользователя.
![image](https://habrastorage.org/getpro/habr/post_images/607/b92/f25/607b92f257cf849c7c3fdc8abbf885d5.png)
4. Протестировал web-телефон на способность звонить. Использовал для этого наши прежние SIP-аккаунты. Все работает, как надо. И на мобильники звонит, и на SIP-телефоны, и удерживание звонков и трансфер, и блэкджек и…
![image](https://habrastorage.org/getpro/habr/post_images/a64/e9c/68a/a64e9c68a573754369e664a5105d3ead.png)
Похожим образом работает созвон с мобильным телефоном.
5. Адаптировал код web-телефона для своей web-CRM, перерисовал его дизайн и теперь он выглядит так:
![image](https://habrastorage.org/getpro/habr/post_images/4b1/41e/2c8/4b141e2c88dd0f700ed852127183137b.png)
На адаптации стоит остановиться подробнее, т.к. перерисовкой дизайна дело не ограничилось.
Первой же серьезной задачей стала автоматическая регистрация web-телефона на SIP сервере. В противном случае, оператору пришлось бы вводить SIP логин и пароль повторно, уже после того, как он ввел логин и пароль для CRM системы. Встал вопрос, как интегрировать.
Оказалось, что в API есть для этого специальная функция loginByToken:
function loginByToken(token) {
trace("Phone - loginByToken "+ token);
connectingViewBeClosed = false;
var result = flashphoner.loginByToken(flashphonerLoader.urlServer, token, document.URL);
closeLoginView();
openConnectingView("Connecting...", 0);
}
Для того чтобы разобраться, как эта функция работает, пришлось хорошо постараться.
C помощью документации и примеров удалось выяснить, что все это работает примерно так:
![image](https://habrastorage.org/getpro/habr/post_images/1af/524/67b/1af52467bb80c370e55414dad521dcea.png)
1) При создании токена на стороне CRM используется алгоритм шифрования AES, которым зашифровывается строка, включающая SIP логин и пароль пользователя, а так же другую необходимую информацию.
Ключ шифрования известен только нашему серверу, где развернута CRM, а так же WCS серверу. Кроме того, срок действия токена задается специальным атрибутом expires для того, чтобы не было возможности им повторно воспользоваться.
Криптование токена происходит в AES CTR mode. Ниже пример c openssl, в котором происходит генерация шифрованного токена с передачей SIP пароля:
echo -ne '<root status="ok" description="test" registerRequired="true" login="user5" authenticationName="user5" password="password" outboundProxy="proxy.my" domain="proxy.my" port="5060" visibleName="AAA" api_key="App1" expires="1394839040761100000"/>' | openssl enc -aes-128-ctr -nosalt -K 8263D535FFFFFFFF7B0F60 -iv 00000000000000000000000000000000 | xxd -p
В результате получил что-то вроде:
CRM:cf4693eedaafda1390b261dcf29d45bd3556d64b1f69cd84db8c3ac8721e7e139b80be75e39da18154e897596e9317084faee0d24d6a6197b62a93a2647b263059167b2664179a5866738260c77372e04fe22104ebe1c7530e9215f50d111fd24384755d28d06673e866159c0b6b83289c045619e8481f9c2a6b56b182f393a7dea06b38b7856436895402a5b40f0525a17822ae0f3204b606e4f0169d1ca9176e8e1b696683d12c7db8208946c204e94f3c8ff285f2bcef4ca9b12187cf541ce37d508d3663ef65f944b01db9aea5c0f10002a376d051cbf1b19bc34f76b6d2a4e1ad1450ae412b51b3af1d3860167f5416b3d2c9eeff94d60b82279e8685beb543893e8a09dee640d7366e478d0d1ee7368e0b63b511
Слева название нашего приложения “CRM”, а справа созданый ранее токен.
Вставляю этот токен в конфиг flashphoner.xml web-телефона в таком виде:
<token>CRM:cf4693eed...</token>
В этом случае процедура автоматической регистрации по токену начнется сразу же после перезагрузки страницы.
2 и 3) loginByToken и расшифровка.
На стороне сервера в конфиге прописаны ключи шифрования для AES:
CRM=8263D535FFFFFFFF7B0F60
Таким образом, когда приходит токен с префиксом “CRM:” для его расшифровки используется соответствующий ключ.
В результате расшифровки WCS сервер получает зашифрованную ранее строку:
<root status="ok" description="test" registerRequired="true" login="user5" authenticationName="user5" password="password" outboundProxy="proxy.my" domain="proxy.my" port="5060" visibleName="AAA" api_key="App1" expires="1394839040761100000"/>
и из этой XML строки берет все данные необходимые для SIP регистрации.
3) Как только сервер расшифровал данные, он посылает SIP REGISTER запрос на SIP и на 401 ответ отдает уже нормальную Digest аутентификацию с использованием расшифрованных на предыдущем шаге SIP логина и пароля.
REGISTER sip:sipnet.ru;lr SIP/2.0
Call-ID: 345ec5157b1a66de3a3a275bdba36197@192.168.1.90
CSeq: 2 REGISTER
From: <sip:crm1@sipnet.ru>;tag=73a499a8
To: <sip:crm1@sipnet.ru>
Via: SIP/2.0/UDP 192.168.1.90:30000;branch=z9hG4bK2622ce723c34760d6a3f43dd631329e1
Max-Forwards: 70
User-Agent: WebRTC
Allow: UPDATE,MESSAGE,BYE,ACK,REFER,INVITE,NOTIFY,INFO,OPTIONS,CANCEL
Contact: <sip:crm1@192.168.1.90:30000>;expires=3600
Expires: 3600
Authorization: Digest
username="crm1",realm="etc.tario.ru",nonce="4A0674BEDF81E0B3F65D",uri="sip:sipnet.ru;lr",response="0762b862c544007f4fb7c43277312a3d",algorithm=MD5,opaque="opaq",qop=auth,cnonce="1234567890",nc=00000001
Content-Length: 0
В этом случае SIP логин и пароль знают только сама CRM и Web Call Server. На браузер эти данные в открытом виде не попадают.
Таким образом мне удалось внедрить телефон в страницу оператора, не заставляя его хранить два разных аккаунта — один для CRM другой для SIP, потому что это очень неудобно. Теперь сразу после загрузки страницы вызывается loginByToken и телефон переходит в состояние готовности.
Некоторые результаты внедрения браузерных звонков:
1. Звонки теперь делаются с сайта и принимаются на сайте, где все действия фиксируются в системе.
2. Стало возможным прослушивание записанных разговоров, что помогает разрешать конфликтные ситуации с клиентами и разногласия между сотрудниками. Это важно для нашего распределенного офиса.
3. Количество принятых звонков увеличилось примерно на 20%. Стало понятно, что операторы не всегда поднимали трубку при звонке клиента.
На текущий момент можно сказать, что все работает, как задумано. Проблемные ситуации удалось разрешить без серьезного погружения в SIP матчасть.
Из недостатков можно отметить невозможность установки под Windows. Кстати, с установкой под Linux и интеграцией тоже пришлось повозиться и, похоже, что осилит ее только продвинутый пользователь/разработчик.
WebRTC аудио звонки работают стабильно и без каких-либо дополнительных браузерных плагинов, типа Flash Player. Так что можно сказать, что мне удалось реализовать задуманную интеграцию и две недели работы были потрачены не зря.