Asterisk: Отправляем коды подтверждения голосом

Двухфакторная авторизация, коды для подтверждения номера телефона – всё это обычно присылают по SMS. Но как быть, когда нужно подтвердить не мобильный номер, а наземный? Он не может принять SMS сообщение, по этому, единственный путь – это голосовой вызов.

Если примеры, когда звонки осуществляются с рандомных номеров, а от юзера требуется ввести последние сколько-то цифр этого номера. Но такой метод требует либо возможности подмены номеров, либо большого пула, из которого можно каждый раз щипать случайны исходящий CID. К сожалению, вышеперечисленное доступно отнюдь не всем. Так что, в нашем случае мы просто заставим “робота” прочитать нужный код вслух.

Эмоциональное отступление

Не так давно ко мне прилетел заказ с ровно таким техзаданием. Но к моему великому сожалению, клиент оказался не совсем адекватным. Он начал на ходу менять уже оговоренное задание, добавляя в него избыточные и нелогичные требования. На мои замечания по этому поводу он отвечал в стиле “я тут босс, мне виднее”. Не смотря на то, что большая часть работы уже была выполнена в виде демонстрационного стенда, я решил показать всю серьёзность своего самолюбия и отказался от этой работы, забрав всё своё с собой. :) А теперь, я решил поделиться своей наработкой с вами, дорогие мои читатели! Ведь иначе получится, что я пилил её напрасно… И я действительно буду рад, если благодаря этому материалу, кто-то из вас заработает на махонькую баночку чёрной икры. ;)

К практике

Предполагается, что перед тем, как вы приступите к чтению данной инструкции, у вас уже будет настроенный сервер на Asterisk и прикрученный к нему выход в телефонную сеть.

  1. Начнём загрузки архива с озвучкой цифр и объявления того, что это за цифры… Тому, кто собирается сделать озвучку на узбекском языке повезло особенно, поскольку я уже сделал это за вас. Качаем архив тут, а затем размещаем его содержимое там, где астериск должен искать звуки. В моём случае это директория /var/lib/asterisk/sounds/custom/
  2. Практически всё, что нам надо будет происходить прямо в контексте. Добавляем этот самый контекст в /etc/asterisk/extensions_custom.conf со следующим содержанием:
[confirm-code]
exten => s,1,Answer()
exten => s,n,Playback(custom/uzcode)
exten => s,n,Playback(custom/uz${N1})
exten => s,n,Playback(custom/uz${N2})
exten => s,n,Playback(custom/uz${N3})
exten => s,n,Playback(custom/uz${N4})
exten => s,n,Playback(custom/confirmcode)
exten => s,n,Playback(custom/ru${N1})
exten => s,n,Playback(custom/ru${N2})
exten => s,n,Playback(custom/ru${N3})
exten => s,n,Playback(custom/ru${N4})
exten => s,n,Hangup()

Вряд-ли тут нужно что-то объяснять, но на всякий случай подскажу, что в начале идёт ответ, а затем последовательное воспроизведение голосовых файлов на двух языках. В моём случае код должен быть фиксированным из четырёх цифр. Соответственно этому, в контексте вы видите по четыре строки с переменными, в которые будет разложен код подтверждения. Вы же можете сделать больше, или меньше, добавить повтор копипастом или вовсе поменять всё местами, чтобы всех запутать и ничего не работало…

3. Нужен интерфейс, через который какой-либо софт извне будет инициировать отправку звонка с кодом пользователю. Выполним его в виде php скрипта, использующего наипростейший метод вызова, при помощи call-файлов.

<?php
	if($_REQUEST['p'] > 1 && $_REQUEST['p'] <= 999999999){
		$phone = $_REQUEST['p'];
	} else {
		$result['status'] = "ERROR";
		$result['msg'] = "INCORRECT_PHONE_NUMBER";
		print(json_encode($result));
		die();
	}
	if(preg_match('/^[0-9]{4}$/',$_REQUEST['c'])){
		for($i=0; $i<4; $i++){
			$num[] = substr($_REQUEST['c'],$i,1);
		}
	} else {
		$result['status'] = "ERROR";
		$result['msg'] = "INCORRECT_CODE";
		print(json_encode($result));
		die();
	}

$callpid = substr(md5(time().rand(5, 5)),0,$callidlen);
$body="
Channel: Local/".$phone."@from-internal
CallerID: Robot <40> // Можно поменять на что-то своё.
MaxRetries: 0
RetryTime: 10
WaitTime: 10
Context: confirm-code
Extension: s
Priority: 1
AlwaysDelete Yes
Setvar: CPID=".$callpid."
Setvar: N1=".$num[0]."
Setvar: N2=".$num[1]."
Setvar: N3=".$num[2]."
Setvar: N4=".$num[3];
	if(file_put_contents("/var/spool/asterisk/outgoing/".md5(time()).".call",$body)){
		$result['status'] = "SUCCESS";
		$result['msg'] = "CALL_SENT_TO_QUEUE";
		$result['callid'] = $callpid;
		print(json_encode($result));
	} else {
		$result['status'] = "ERROR";
		$result['msg'] = "Can't create call file.";
	}
?>

Обратите внимание на проверку номера “B”. В моём скрипте она не полноценная, поскольку делалась для тестирования, в том числе на внутренних номерах, так что настоятельно рекомендую переписать это под свои условия.

Так-же, убедитесь что у пользователя, от имени которого запускается скрипт есть права на запись в директорию /var/spool/asterisk/outgoing/. После того, как Asterisk заметит call файл в этой директории, он попытается его обработать, а затем тут-же удалит. Момент обработки и возможные ошибки подробно видны в консоли отладки астера.

Если всё выполнено правильно, то вызов строки типа http://your.pbx.addr/call.php?p=номер&c=1234 заставит ваш сервер совершить желаемый звоночек.