diff --git a/composer.lock b/composer.lock index 5f404a2c..5f81e9d0 100644 --- a/composer.lock +++ b/composer.lock @@ -111,16 +111,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.0", + "version": "6.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5" + "reference": "43ece0e75098b7ecd8d13918293029e555a50f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5", - "reference": "dbc2bc3a293ed6b1ae08a3651e2bfd213d19b6a5", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", + "reference": "43ece0e75098b7ecd8d13918293029e555a50f82", "shasum": "" }, "require": { @@ -174,7 +174,7 @@ "rest", "web service" ], - "time": "2019-12-07T18:20:45+00:00" + "time": "2019-12-23T11:57:10+00:00" }, { "name": "guzzlehttp/promises", diff --git a/doc/Admin Documentation.md b/doc/Admin Documentation.md index f737e955..de61069e 100644 --- a/doc/Admin Documentation.md +++ b/doc/Admin Documentation.md @@ -119,6 +119,32 @@ Interactive admin configuration: occ twofactorauth:gateway:configure sms ``` +### OVH +URL: https://www.ovhtelecom.fr/sms/ +Stability: Experimental + +Use the SMS gateway provided by OVH for sending SMS. + +1. First create an application key, an application secret and a consumer key with the [createToken](https://eu.api.ovh.com/createToken/index.cgi?GET=/sms&GET=/sms/*/jobs&POST=/sms/*/jobs) page. + +2. Go to you OVH account manager and get an SMS plan. You should see on the sidebar menu the SMS submenu with the account name: *sms-#######* + +3. Create a "sender". On the main page of the SMS account, you should see a *Create a sender* link. + +4. Interactive admin configuration: +```bash +occ twofactorauth:gateway:configure sms +``` + + * Choose the `ovh` SMS provider. + * Choose the endpoint connexion. + * Enter successively the application key, the application secret, the consumer key, the account, and the sender. + +5. Try to send a test with +```bash +occ twofactorauth:gateway:test sms +``` + [User Documentation]: https://nextcloud-twofactor-gateway.readthedocs.io/en/latest/User%20Documentation/ ### Spryng diff --git a/lib/Command/Configure.php b/lib/Command/Configure.php index 2c1ac0be..e93c8e27 100644 --- a/lib/Command/Configure.php +++ b/lib/Command/Configure.php @@ -32,6 +32,7 @@ use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\EcallSMSConfig; use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\PlaySMSConfig; use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\Sms77IoConfig; +use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\OvhConfig; use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\WebSmsConfig; use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\PuzzelSMSConfig; use OCA\TwoFactorGateway\Service\Gateway\SMS\Provider\HuaweiE3531Config; @@ -105,7 +106,8 @@ private function configureSignal(InputInterface $input, OutputInterface $output) private function configureSms(InputInterface $input, OutputInterface $output) { $helper = $this->getHelper('question'); - $providerQuestion = new Question('Please choose a SMS provider (websms, playsms, clockworksms, puzzelsms, ecallsms, voipms, huawei_e3531, spryng, sms77io): ', 'websms'); + + $providerQuestion = new Question('Please choose a SMS provider (websms, playsms, clockworksms, puzzelsms, ecallsms, voipms, huawei_e3531, spryng, sms77io, ovh', 'websms'); $provider = $helper->ask($input, $output, $providerQuestion); /** @var SMSConfig $config */ @@ -247,6 +249,39 @@ private function configureSms(InputInterface $input, OutputInterface $output) { $providerConfig->setApiKey($apiKey); break; + case 'ovh': + $config->setProvider($provider); + + /** @var OvhConfig $providerConfig */ + $providerConfig = $config->getProvider()->getConfig(); + + $endpointQ = new Question('Please enter the endpoint to use (ovh-eu, ovh-us, ovh-ca, soyoustart-eu, soyoustart-ca, kimsufi-eu, kimsufi-ca, runabove-ca): '); + $endpoint = $helper->ask($input, $output, $endpointQ); + + $appKeyQ = new Question('Please enter your application key: '); + $appKey = $helper->ask($input, $output, $appKeyQ); + + $appSecretQ = new Question('Please enter your application secret: '); + $appSecret = $helper->ask($input, $output, $appSecretQ); + + $consumerKeyQ = new Question('Please enter your consumer key: '); + $consumerKey = $helper->ask($input, $output, $consumerKeyQ); + + $accountQ = new Question('Please enter your account (sms-*****): '); + $account = $helper->ask($input, $output, $accountQ); + + $senderQ = new Question('Please enter your sender: '); + $sender = $helper->ask($input, $output, $senderQ); + + $providerConfig->setApplicationKey($appKey); + $providerConfig->setApplicationSecret($appSecret); + $providerConfig->setConsumerKey($consumerKey); + $providerConfig->setEndpoint($endpoint); + $providerConfig->setAccount($account); + $providerConfig->setSender($sender); + break; + + default: $output->writeln("Invalid provider $provider"); break; diff --git a/lib/Service/Gateway/SMS/Provider/Ovh.php b/lib/Service/Gateway/SMS/Provider/Ovh.php new file mode 100644 index 00000000..6aec22ae --- /dev/null +++ b/lib/Service/Gateway/SMS/Provider/Ovh.php @@ -0,0 +1,185 @@ + + * + * Nextcloud - Two-factor Gateway for Ovh + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\TwoFactorGateway\Service\Gateway\SMS\Provider; + +use Exception; +use OCA\TwoFactorGateway\Exception\SmsTransmissionException; +use OCA\TwoFactorGateway\Exception\InvalidSmsProviderException; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; + +class Ovh implements IProvider { + + const PROVIDER_ID = 'ovh'; + + /** @var IClient */ + private $client; + + /** @var OvhConfig */ + private $config; + + /** + * Url to communicate with Ovh API + * + * @var array + */ + private $endpoints = [ + 'ovh-eu' => 'https://api.ovh.com/1.0', + 'ovh-us' => 'https://api.us.ovhcloud.com/1.0', + 'ovh-ca' => 'https://ca.api.ovh.com/1.0', + 'kimsufi-eu' => 'https://eu.api.kimsufi.com/1.0', + 'kimsufi-ca' => 'https://ca.api.kimsufi.com/1.0', + 'soyoustart-eu' => 'https://eu.api.soyoustart.com/1.0', + 'soyoustart-ca' => 'https://ca.api.soyoustart.com/1.0', + 'runabove-ca' => 'https://api.runabove.com/1.0', + ]; + + /** + * Array of the 4 needed parameters to connect to the API + * @var array + */ + private $attrs = [ + 'AK' => null, + 'AS' => null, + 'CK' => null, + 'endpoint' => null, + 'timedelta' => null + ]; + + + public function __construct(IClientService $clientService, + OvhConfig $config) { + $this->client = $clientService->newClient(); + $this->config = $config; + } + + /** + * @param string $identifier + * @param string $message + * + * @throws SmsTransmissionException + */ + public function send(string $identifier, string $message) { + $config = $this->getConfig(); + $endpoint = $config->getEndpoint(); + $sender = $config->getSender(); + $smsAccount = $config->getAccount(); + + $this->attrs['AK'] = $config->getApplicationKey(); + $this->attrs['AS'] = $config->getApplicationSecret(); + $this->attrs['CK'] = $config->getConsumerKey(); + if (!isset($this->endpoints[$endpoint])) + throw new InvalidSmsProviderException("Endpoint $endpoint not found"); + $this->attrs['endpoint'] = $this->endpoints[$endpoint]; + + $this->getTimeDelta(); + + $header = $this->getHeader('GET',$this->attrs['endpoint'].'/sms'); + $response = $this->client->get($this->attrs['endpoint'].'/sms',[ + 'headers' => $header, + ]); + $smsServices = json_decode($response->getBody(),true); + + $smsAccountFound = false; + foreach ($smsServices as $smsService) { + if ($smsService === $smsAccount) { + $smsAccountFound = true; + break; + } + } + if ($smsAccountFound === false) { + throw new InvalidSmsProviderException("SMS account $smsAccount not found"); + } + $content = [ + "charset"=> "UTF-8", + "message"=> $message, + "noStopClause"=> true, + "priority"=> "high", + "receivers"=> [ $identifier ], + "senderForResponse"=> false, + "sender"=> $sender, + "validityPeriod"=> 3600 + ]; + $body = json_encode($content); + + $header = $this->getHeader('POST',$this->attrs['endpoint']."/sms/$smsAccount/jobs",$body); + $response = $this->client->post($this->attrs['endpoint']."/sms/$smsAccount/jobs",[ + 'headers' => $header, + 'json' => $content, + ]); + $resultPostJob = json_decode($response->getBody(),true); + + if (count($resultPostJob["validReceivers"]) === 0) { + throw new SmsTransmissionException("Bad receiver $identifier"); + } + } + + /** + * @return OvhConfig + */ + public function getConfig(): IProviderConfig { + return $this->config; + } + + /** + * Compute time delta between this server and OVH endpoint + * @throws InvalidSmsProviderException + */ + private function getTimeDelta() { + if (!isset($this->attrs['timedelta'])) { + if (!isset($this->attrs['endpoint'])) + throw new InvalidSmsProviderException('Need to set the endpoint'); + try { + $response = $this->client->get($this->attrs['endpoint'].'/auth/time'); + $serverTimestamp = (int)$response->getBody(); + $this->attrs['timedelta'] = $serverTimestamp - time(); + } + catch (Exception $ex) { + throw new InvalidSmsProviderException('Unable to calculate time delta:'.$ex->getMessage()); + } + } + } + + /** + * Make header for Ovh + * @param string $method The methode use for the query : GET, POST, PUT, DELETE + * @param string $query The fulle URI for the query: https://eu.api.ovh.com/1.0/...... + * @param string $body JSON encoded body content for the POST request + * @return array $header Contains the data for the request need by OVH + */ + private function getHeader($method,$query,$body='') { + $timestamp = time() + $this->attrs['timedelta']; + $prehash = $this->attrs['AS'].'+'.$this->attrs['CK'].'+'.$method.'+'.$query.'+'.$body.'+'.$timestamp; + $header = [ + 'Content-Type' => 'application/json; charset=utf-8', + 'X-Ovh-Application' => $this->attrs['AK'], + 'X-Ovh-Timestamp' => $timestamp, + 'X-Ovh-Signature' => '$1$'.sha1($prehash), + 'X-Ovh-Consumer' => $this->attrs['CK'], + ]; + return $header; + } + + +} diff --git a/lib/Service/Gateway/SMS/Provider/OvhConfig.php b/lib/Service/Gateway/SMS/Provider/OvhConfig.php new file mode 100644 index 00000000..4703bbde --- /dev/null +++ b/lib/Service/Gateway/SMS/Provider/OvhConfig.php @@ -0,0 +1,109 @@ + + * + * Nextcloud - Two-factor Gateway for Ovh + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\TwoFactorGateway\Service\Gateway\SMS\Provider; + +use function array_intersect; +use OCA\TwoFactorGateway\AppInfo\Application; +use OCA\TwoFactorGateway\Exception\ConfigurationException; +use OCP\IConfig; + +class OvhConfig implements IProviderConfig { + + /** @var IConfig */ + private $config; + + public function __construct(IConfig $config) { + $this->config = $config; + } + + private function getOrFail(string $key): string { + $val = $this->config->getAppValue(Application::APP_NAME, $key, null); + if (is_null($val)) { + throw new ConfigurationException(); + } + return $val; + } + + public function getApplicationKey(): string { + return $this->getOrFail('ovh_application_key'); + } + + public function getApplicationSecret(): string { + return $this->getOrFail('ovh_application_secret'); + } + + public function getConsumerKey(): string { + return $this->getOrFail('ovh_consumer_key'); + } + + public function getEndpoint(): string { + return $this->getOrFail('ovh_endpoint'); + } + + public function getAccount(): string { + return $this->getOrFail('ovh_account'); + } + + public function getSender(): string { + return $this->getOrFail('ovh_sender'); + } + + public function setApplicationKey(string $appKey){ + $this->config->setAppValue(Application::APP_NAME, 'ovh_application_key', $appKey); + } + + public function setApplicationSecret(string $appSecret){ + $this->config->setAppValue(Application::APP_NAME, 'ovh_application_secret', $appSecret); + } + + public function setConsumerKey(string $consumerKey){ + $this->config->setAppValue(Application::APP_NAME, 'ovh_consumer_key', $consumerKey); + } + + public function setEndpoint(string $endpoint){ + $this->config->setAppValue(Application::APP_NAME, 'ovh_endpoint', $endpoint); + } + + public function setAccount($account){ + $this->config->setAppValue(Application::APP_NAME, 'ovh_account', $account); + } + + public function setSender($sender){ + $this->config->setAppValue(Application::APP_NAME, 'ovh_sender', $sender); + } + + public function isComplete(): bool { + $set = $this->config->getAppKeys(Application::APP_NAME); + $expected = [ + 'ovh_application_key', + 'ovh_application_secret', + 'ovh_consumer_key', + 'ovh_endpoint', + 'ovh_account', + 'ovh_sender' + ]; + return count(array_intersect($set, $expected)) === count($expected); + } + +} diff --git a/lib/Service/Gateway/SMS/Provider/ProviderFactory.php b/lib/Service/Gateway/SMS/Provider/ProviderFactory.php index b27c341c..d3f3614b 100644 --- a/lib/Service/Gateway/SMS/Provider/ProviderFactory.php +++ b/lib/Service/Gateway/SMS/Provider/ProviderFactory.php @@ -53,6 +53,8 @@ public function getProvider(string $id): IProvider { return $this->container->query(HuaweiE3531::class); case Sms77Io::PROVIDER_ID: return $this->container->query(Sms77Io::class); + case Ovh::PROVIDER_ID: + return $this->container->query(Ovh::class); case SpryngSMS::PROVIDER_ID: return $this->container->query(SpryngSMS::class); default: