From a39c9577b122ecf39418f54132a4d8248acd1f2d Mon Sep 17 00:00:00 2001 From: osaajani <> Date: Fri, 5 Nov 2021 23:26:09 +0100 Subject: [PATCH] Add adapter for kannel phones --- VERSION | 2 +- adapters/BenchmarkAdapter.php | 1 + adapters/KannelAdapter.php | 484 ++++++++++++++++++++++ adapters/OctopushShortcodeAdapter.php | 2 + adapters/OctopushVirtualNumberAdapter.php | 2 + controllers/internals/Tool.php | 20 + daemons/Webhook.php | 3 +- 7 files changed, 512 insertions(+), 2 deletions(-) create mode 100644 adapters/KannelAdapter.php diff --git a/VERSION b/VERSION index 040943e..b57c3c6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.2.1 +v3.2.2 diff --git a/adapters/BenchmarkAdapter.php b/adapters/BenchmarkAdapter.php index e9c5195..6cbd3da 100644 --- a/adapters/BenchmarkAdapter.php +++ b/adapters/BenchmarkAdapter.php @@ -172,6 +172,7 @@ namespace adapters; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_POST, true); diff --git a/adapters/KannelAdapter.php b/adapters/KannelAdapter.php new file mode 100644 index 0000000..a971489 --- /dev/null +++ b/adapters/KannelAdapter.php @@ -0,0 +1,484 @@ + + * + * This source file is subject to the GPL-3.0 license that is bundled + * with this source code in the file LICENSE. + */ + +namespace adapters; + +use controllers\internals\Tool; +use descartes\Router; + +/** + * Kannel adapter. + */ +class KannelAdapter implements AdapterInterface +{ + const KANNEL_SENDSMS_RESULTS_ACCEPTED = 0; + const KANNEL_SENDSMS_RESULTS_QUEUED = 3; + + const KANNEL_SENDSMS_HTTP_CODE_ACCEPTED = 202; + const KANNEL_SENDSMS_HTTP_CODE_QUEUED = 202; + + /** + * DLR mask to transmit to kannel + * + * 1 -> Delivered to phone + * 2 -> not delivered + * 16 -> non delivered to SMSC + * + * (see https://gist.github.com/grantpullen/3d550f31c454e80fda8fc0d5b9105fd0) + */ + const KANNEL_DLR_BITMASK = 1 + 2 + 16; + + /** + * Data used to configure interaction with the implemented service. (e.g : Api credentials, ports numbers, etc.). + */ + private $data; + + /** + * Kannel send-sms service url + */ + private $kannel_sendsms_url; + + /** + * Kannel send-sms username. + */ + private $username; + + /** + * Kannel send-sms password. + */ + private $password; + + /** + * Phone number of the sender, this number may or may not actually be overrided by the SMSC + */ + private $from; + + /** + * SMSC's id to use for sending the message + */ + private $smsc; + + /** + * SMS Delivery Report Url + */ + private $dlr_url; + + /** + * Adapter constructor, called when instanciated by RaspiSMS. + * + * @param string $number : Phone number the adapter is used for + * @param json string $data : JSON string of the data to configure interaction with the implemented service + */ + public function __construct(string $data) + { + $this->data = json_decode($data, true); + + $this->kannel_sendsms_url = $this->data['kannel_sendsms_url']; + $this->username = $this->data['username']; + $this->password = $this->data['password']; + $this->from = $this->data['from']; + $this->dlr_url = $this->data['dlr_url']; + + $this->smsc = $this->data['smsc'] ?? null; + } + + /** + * Classname of the adapter. + */ + public static function meta_classname(): string + { + return __CLASS__; + } + + /** + * Uniq name of the adapter + * It should be the classname of the adapter un snakecase. + */ + public static function meta_uid(): string + { + return 'kannel_adapter'; + } + + /** + * Should this adapter be hidden in user interface for phone creation and + * available to creation through API only. + */ + public static function meta_hidden(): bool + { + return false; + } + + /** + * Name of the adapter. + * It should probably be the name of the service it adapt (e.g : Gammu SMSD, OVH SMS, SIM800L, etc.). + */ + public static function meta_name(): string + { + return 'Kannel'; + } + + /** + * Description of the adapter. + * A short description of the service the adapter implements. + */ + public static function meta_description(): string + { + $kannel_homepage = 'https://www.kannel.org'; + + return ' + Envoi de SMS via le logiciel Kannel, pour plus d\'information sur Kannel, voir le site du projet.
+ Pour plus d\'information sur l\'utilisation de ce type de téléphone, reportez-vous à la documentation sur le téléphone "Kannel". + '; + } + + /** + * List of entries we want in data for the adapter. + * + * @return array : Every line is a field as an array with keys : name, title, description, required + */ + public static function meta_data_fields(): array + { + return [ + [ + 'name' => 'kannel_sendsms_url', + 'title' => 'Adresse URL du service kannel sendsms', + 'description' => 'Adresse URL du service sendsms de Kannel (ex : http://smsbox.host.name:13013/cgi-bin/sendsms)', + 'required' => true, + ], + [ + 'name' => 'username', + 'title' => 'Nom de l\'utilisateur', + 'description' => 'Nom d\'utilisateur du service send-sms de Kannel.', + 'required' => true, + ], + [ + 'name' => 'password', + 'title' => 'Mot de passe de l\'utilisateur', + 'description' => 'Mot de passe de l\'utilisateur du service send-sms de Kannel.', + 'required' => true, + ], + [ + 'name' => 'from', + 'title' => 'Numéro de téléphone émetteur ou nom de l\'émetteur', + 'description' => 'Numéro de téléphone à transmettre au SMS Center, ou nom à afficher à la place du numéro (dans ce cas, entre 3 et 11 caractères), dans la très grande majorité des cas, ce numéro ou ce nom sera écrasé par le SMSC.', + 'required' => true, + ], + [ + 'name' => 'dlr_url', + 'title' => 'Adresse URL de livraison du Delivery Report du SMS', + 'description' => 'Adresse URL de livraison du Delivery Report du SMS qui sera transmis à Kannel. Vous devriez probablement laisser ce champs tel quel.', + 'required' => true, + 'default_value' => \descartes\Router::url('Callback', 'update_sended_status', ['adapter_uid' => self::meta_uid()], ['api_key' => $_SESSION['user']['api_key'] ?? '']), + ], + [ + 'name' => 'smsc', + 'title' => 'Identifiant unique du SMSC', + 'description' => 'Identifiant du SMSC (sms-id) à utiliser pour envoyer le message.
+ Laissez vide pour laisser Kannel décider du routage vers le SMSC.', + 'required' => false, + ], + ]; + } + + /** + * Does the implemented service support reading smss. + */ + public static function meta_support_read(): bool + { + return false; + } + + /** + * Does the implemented service support flash smss. + */ + public static function meta_support_flash(): bool + { + return false; + } + + /** + * Does the implemented service support status change. + */ + public static function meta_support_status_change(): bool + { + return true; + } + + /** + * Does the implemented service support reception callback. + */ + public static function meta_support_reception(): bool + { + return true; + } + + /** + * Does the implemented service support mms reception. + */ + public static function meta_support_mms_reception(): bool + { + return false; + } + + /** + * Does the implemented service support mms sending. + */ + public static function meta_support_mms_sending(): bool + { + return false; + } + + public static function meta_support_inbound_call_callback(): bool + { + return false; + } + + public static function meta_support_end_call_callback(): bool + { + return false; + } + + public function send(string $destination, string $text, bool $flash = false, bool $mms = false, array $medias = []): array + { + $response = [ + 'error' => false, + 'error_message' => null, + 'uid' => null, + ]; + + try + { + //As kannel does not return uid of the SMS when sending it, we create our own uid and will pass it to kannel's delivery report url + //in order to retrieve it in raspisms and update the status + $sms_uid = Tool::random_uuid(); + + + //Forge dlr Url by adding new query parts to url provided within phone settings + $dlr_url_parts = parse_url($this->dlr_url); + + //Append sms uid and delivery report value to the original dlr_url query parts + $dlr_url_parts['query'] = $dlr_url_parts['query'] ?? ''; + $dlr_url_query_parts = []; + parse_str($dlr_url_parts['query'], $dlr_url_query_parts); + unset($dlr_url_query_parts['type']); + $dlr_url_query_parts['sms_uid'] = $sms_uid; //Pass uid as param so raspisms can identify sms to update + $dlr_url_parts['query'] = http_build_query($dlr_url_query_parts) . '&type=%d'; //Kannel will replace %d by the delivery report value. We cannot set type in bild query or it get double encoded + + $forged_dlr_url = Tool::unparse_url($dlr_url_parts); + + + $data = [ + 'username' => $this->username, + 'password' => $this->password, + 'text' => $text, + 'to' => $destination, + 'from' => $this->from, + 'dlr-mask' => self::KANNEL_DLR_BITMASK, + 'dlr-url' => $forged_dlr_url, + ]; + + if ($this->smsc) + { + $data['smsc'] = $this->smsc; + } + + $endpoint = $this->kannel_sendsms_url . '?' . http_build_query($data); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + $curl_response = curl_exec($curl); + $http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if (false === $curl_response) + { + $response['error'] = true; + $response['error_message'] = 'HTTP query failed.'; + + return $response; + } + + if (!in_array($http_code, [self::KANNEL_SENDSMS_HTTP_CODE_ACCEPTED, self::KANNEL_SENDSMS_HTTP_CODE_QUEUED])) + { + $response['error'] = true; + $response['error_message'] = 'Response error with HTTP code : ' . $http_code . ' -> ' . $curl_response; + + return $response; + } + + $response['uid'] = $sms_uid; + + return $response; + } + catch (\Throwable $t) + { + $response['error'] = true; + $response['error_message'] = $t->getMessage(); + + return $response; + } + } + + public function read(): array + { + return []; + } + + public function test(): bool + { + try + { + if (!$this->username || !$this->password || !$this->from || !$this->dlr_url) + { + return false; + } + + //Check kannel url is a valid http/https url to protect against ssrf + //This is mainly cosmetic, the real protection is in CURLOPT_PROTOCOLS + if (!mb_ereg_match('^http(s?)://', $this->kannel_sendsms_url)) + { + return false; + } + + //Check credentials and kannel url + $data = [ + 'username' => $this->username, + 'password' => $this->password, + ]; + + $endpoint = $this->kannel_sendsms_url . '?' . http_build_query($data); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); //Protect curl against non http(s) queries and redirects + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + $curl_response = curl_exec($curl); + $http_code = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if (false === $curl_response) + { + return false; + } + + switch (true) + { + case 403 == $http_code : //Bad credentials + case 404 == $http_code : //Cannot find url + return false; + + case $http_code >= 500 : //Server error + return false; + } + + if (!filter_var($this->dlr_url, FILTER_VALIDATE_URL)) + { + return false; + } + + return true; + } + catch (\Throwable $t) + { + return false; + } + } + + public static function status_change_callback() + { + $status = $_GET['type'] ?? false; + $uid = $_GET['sms_uid'] ?? false; + + if (!$status || !$uid) + { + return false; + } + + switch ((int) $status) + { + case 1: + $status = \models\Sended::STATUS_DELIVERED; + + break; + + case 2: + case 16: + $status = \models\Sended::STATUS_FAILED; + + break; + + default: + $status = \models\Sended::STATUS_UNKNOWN; + + break; + } + + return ['uid' => $uid, 'status' => $status]; + } + + public static function reception_callback(): array + { + $response = [ + 'error' => false, + 'error_message' => null, + 'sms' => null, + ]; + + header('Connection: close'); + header('Content-Encoding: none'); + header('Content-Length: 0'); + + $text = file_get_contents('php://input'); + $number = $_SERVER['HTTP_X_KANNEL_TO'] ?? false; + $at = $_SERVER['HTTP_X_KANNEL_TIME'] ?? false; + + if (!$number || !$text || !$at) + { + $response['error'] = true; + $response['error_message'] = 'One required data of the callback is missing.'; + + return $response; + } + + $origin = \controllers\internals\Tool::parse_phone($number); + if (!$origin) + { + $response['error'] = true; + $response['error_message'] = 'Invalid origin number : ' . $number; + + return $response; + } + + $response['sms'] = [ + 'at' => $at, + 'text' => $text, + 'origin' => $origin, + ]; + + return $response; + } + + public function inbound_call_callback(): array + { + return []; + } + + public function end_call_callback(): array + { + return []; + } +} diff --git a/adapters/OctopushShortcodeAdapter.php b/adapters/OctopushShortcodeAdapter.php index afa4802..8e76106 100644 --- a/adapters/OctopushShortcodeAdapter.php +++ b/adapters/OctopushShortcodeAdapter.php @@ -253,6 +253,7 @@ class OctopushShortcodeAdapter implements AdapterInterface $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_POST, true); @@ -341,6 +342,7 @@ class OctopushShortcodeAdapter implements AdapterInterface $endpoint = $this->api_url . '/wallet/check-balance'; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); diff --git a/adapters/OctopushVirtualNumberAdapter.php b/adapters/OctopushVirtualNumberAdapter.php index d758f8a..3371d20 100644 --- a/adapters/OctopushVirtualNumberAdapter.php +++ b/adapters/OctopushVirtualNumberAdapter.php @@ -244,6 +244,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_POST, true); @@ -333,6 +334,7 @@ class OctopushVirtualNumberAdapter implements AdapterInterface $endpoint = $this->api_url . '/wallet/check-balance'; $curl = curl_init(); curl_setopt($curl, CURLOPT_URL, $endpoint); + curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); diff --git a/controllers/internals/Tool.php b/controllers/internals/Tool.php index 26ec6d9..d677721 100644 --- a/controllers/internals/Tool.php +++ b/controllers/internals/Tool.php @@ -372,4 +372,24 @@ namespace controllers\internals; return $new_dir; } + + /** + * Forge back an url parsed with PHP parse_url function + * + * @param array $parsed_url : Parsed url returned by parse_url function + * @return string : The url as a string + */ + public static function unparse_url(array $parsed_url) + { + $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; + $host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; + $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; + $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; + $pass = ($user || $pass) ? "$pass@" : ''; + $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; + $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; + $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; + return "$scheme$user$pass$host$port$path$query$fragment"; + } } diff --git a/daemons/Webhook.php b/daemons/Webhook.php index 62c43c1..180d8cf 100644 --- a/daemons/Webhook.php +++ b/daemons/Webhook.php @@ -11,6 +11,7 @@ namespace daemons; +use GuzzleHttp\Promise\Utils; use Monolog\Handler\StreamHandler; use Monolog\Logger; @@ -84,7 +85,7 @@ class Webhook extends AbstractDaemon try { - $responses = \GuzzleHttp\Promise\unwrap($promises); + $responses = Utils::unwrap($promises); } catch (\Exception $e) {