From 20f8fb4c457dcb1288137eea9cb5b611c807523c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Tue, 11 Jan 2022 16:07:55 +0100 Subject: [PATCH] Add a send message endpoint, passing by the linphone-daemon unix pipe Import ReactPHP Socket and required dependencies Add a new configuration variable to define the unix pipe path Generalize the API Key to all the users, add a new endpoint to retrieve it, update the documentation Update the dependencies Update the documentation Complete the tests --- cron/flexiapi.debian | 3 +- cron/flexiapi.redhat | 3 +- flexiapi/.env.example | 4 + flexiapi/README.md | 8 +- flexiapi/app/Account.php | 2 + .../app/Console/Commands/ClearApiKeys.php | 49 ++ ...ClearExpiredNonces.php => ClearNonces.php} | 6 +- .../Controllers/Account/AccountController.php | 8 + .../Controllers/Admin/AccountController.php | 8 - .../Controllers/Api/AccountController.php | 12 + .../Controllers/Api/MessageController.php | 50 ++ flexiapi/app/Http/Kernel.php | 4 +- .../Middleware/AuthenticateDigestOrKey.php | 13 +- flexiapi/composer.json | 8 +- flexiapi/composer.lock | 582 +++++++++++++++++- flexiapi/composer.phar | Bin 2356380 -> 2361099 bytes flexiapi/config/app.php | 13 +- ..._last_used_at_column_to_api_keys_table.php | 31 + flexiapi/phpunit.xml | 2 + .../resources/views/account/panel.blade.php | 38 +- .../api/documentation_markdown.blade.php | 26 +- flexiapi/routes/api.php | 6 + flexiapi/routes/web.php | 5 +- flexiapi/tests/Feature/AccountApiKeyTest.php | 59 ++ flexiapi/tests/Feature/AccountApiTest.php | 1 - .../tests/Feature/AccountContactsTest.php | 1 - flexiapi/tests/Feature/AccountMessageTest.php | 58 ++ .../tests/Feature/AccountPhoneChangeTest.php | 4 +- .../tests/Feature/AccountProvisioningTest.php | 1 - flexisip-account-manager.spec | 2 +- 30 files changed, 954 insertions(+), 53 deletions(-) create mode 100644 flexiapi/app/Console/Commands/ClearApiKeys.php rename flexiapi/app/Console/Commands/{ClearExpiredNonces.php => ClearNonces.php} (88%) create mode 100644 flexiapi/app/Http/Controllers/Api/MessageController.php create mode 100644 flexiapi/database/migrations/2022_02_07_111109_add_last_used_at_column_to_api_keys_table.php create mode 100644 flexiapi/tests/Feature/AccountApiKeyTest.php create mode 100644 flexiapi/tests/Feature/AccountMessageTest.php diff --git a/cron/flexiapi.debian b/cron/flexiapi.debian index b529a5c..465e607 100644 --- a/cron/flexiapi.debian +++ b/cron/flexiapi.debian @@ -1,6 +1,7 @@ #!/bin/sh cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/ -sudo -su www-data && php artisan digest:expired-nonces-clear 60 +sudo -su www-data && php artisan digest:clear-nonces 60 +sudo -su www-data && php artisan accounts:clear-api-keys 60 sudo -su www-data && php artisan accounts:clear-accounts-tombstones 7 --apply sudo -su www-data && php artisan accounts:clear-unconfirmed 30 --apply \ No newline at end of file diff --git a/cron/flexiapi.redhat b/cron/flexiapi.redhat index 6793c50..4f73875 100644 --- a/cron/flexiapi.redhat +++ b/cron/flexiapi.redhat @@ -1,6 +1,7 @@ #!/bin/sh cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/ -scl enable rh-php73 "php artisan digest:expired-nonces-clear 60" +scl enable rh-php73 "php artisan digest:clear-nonces 60" +scl enable rh-php73 "php artisan accounts:clear-api-keys 60" scl enable rh-php73 "php artisan accounts:clear-accounts-tombstones 7 --apply" scl enable rh-php73 "php artisan accounts:clear-unconfirmed 30 --apply" \ No newline at end of file diff --git a/flexiapi/.env.example b/flexiapi/.env.example index 9a25a1f..8a16f10 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -4,9 +4,13 @@ APP_KEY= APP_DEBUG=false APP_URL=http://localhost APP_SIP_DOMAIN=sip.example.com + APP_FLEXISIP_PROXY_PID=/var/run/flexisip-proxy.pid +APP_LINPHONE_DAEMON_UNIX_PATH= APP_FLEXISIP_PUSHER_PATH= +APP_API_KEY_EXPIRATION_MINUTES=60 # Number of minutes the generated API Keys are valid + # Risky toggles APP_EVERYONE_IS_ADMIN=false # Allow any accounts to request the API as an administrator APP_ADMINS_MANAGE_MULTI_DOMAINS=false # Allow admins to handle all the accounts in the database diff --git a/flexiapi/README.md b/flexiapi/README.md index 375c7de..4e251f6 100644 --- a/flexiapi/README.md +++ b/flexiapi/README.md @@ -143,11 +143,17 @@ Several other parameters are also available to customize the migration process, php artisan -h db:import +### Clear the expired API Keys + +This will remove the API Keys that were not used after `x minutes`. + + php artisan digest:clear-api-keys {minutes} + ### Clear Expired Nonces for DIGEST authentication This will remove the nonces stored that were not updated after `x minutes`. - php artisan digest:expired-nonces-clear {minutes} + php artisan digest:clear-nonces {minutes} ### Remove the unconfirmed accounts diff --git a/flexiapi/app/Account.php b/flexiapi/app/Account.php index 9c6a011..0f5dea7 100644 --- a/flexiapi/app/Account.php +++ b/flexiapi/app/Account.php @@ -31,6 +31,7 @@ use App\Password; use App\EmailChanged; use App\Helpers\Utils; use App\Mail\ChangingEmail; +use Carbon\Carbon; class Account extends Authenticatable { @@ -223,6 +224,7 @@ class Account extends Authenticatable $apiKey = new ApiKey; $apiKey->account_id = $this->id; + $apiKey->last_used_at = Carbon::now(); $apiKey->key = Str::random(40); $apiKey->save(); } diff --git a/flexiapi/app/Console/Commands/ClearApiKeys.php b/flexiapi/app/Console/Commands/ClearApiKeys.php new file mode 100644 index 0000000..91b11f7 --- /dev/null +++ b/flexiapi/app/Console/Commands/ClearApiKeys.php @@ -0,0 +1,49 @@ +. +*/ + +namespace App\Console\Commands; + +use App\ApiKey; +use Illuminate\Console\Command; + +use Carbon\Carbon; + +class ClearApiKeys extends Command +{ + protected $signature = 'accounts:clear-api-keys {minutes?}'; + protected $description = 'Clear the expired API Keys after n minutes'; + + public function __construct() + { + parent::__construct(); + } + + public function handle() + { + $minutes = $this->argument('minutes') ?? config('app.api_key_expiration_minutes'); + + $this->info('Deleting api keys unused after ' . $minutes . ' minutes'); + + $count = ApiKey::where('last_used_at', '<', + Carbon::now()->subMinutes($minutes)->toDateTimeString() + )->delete(); + + $this->info($count . ' api keys deleted'); + } +} diff --git a/flexiapi/app/Console/Commands/ClearExpiredNonces.php b/flexiapi/app/Console/Commands/ClearNonces.php similarity index 88% rename from flexiapi/app/Console/Commands/ClearExpiredNonces.php rename to flexiapi/app/Console/Commands/ClearNonces.php index 93638e0..4dd9b50 100644 --- a/flexiapi/app/Console/Commands/ClearExpiredNonces.php +++ b/flexiapi/app/Console/Commands/ClearNonces.php @@ -24,10 +24,10 @@ use Illuminate\Console\Command; use Carbon\Carbon; use App\DigestNonce; -class ClearExpiredNonces extends Command +class ClearNonces extends Command { - protected $signature = 'digest:expired-nonces-clear {minutes}'; - protected $description = 'Clear the expired DIGEST nonces'; + protected $signature = 'digest:clear-nonces {minutes}'; + protected $description = 'Clear the expired DIGEST nonces after n minutes'; public function __construct() { diff --git a/flexiapi/app/Http/Controllers/Account/AccountController.php b/flexiapi/app/Http/Controllers/Account/AccountController.php index b3e4d52..e9d437a 100644 --- a/flexiapi/app/Http/Controllers/Account/AccountController.php +++ b/flexiapi/app/Http/Controllers/Account/AccountController.php @@ -54,6 +54,14 @@ class AccountController extends Controller ]); } + public function generateApiKey(Request $request) + { + $account = $request->user(); + $account->generateApiKey(); + + return redirect()->back(); + } + public function delete(Request $request) { return view('account.delete', [ diff --git a/flexiapi/app/Http/Controllers/Admin/AccountController.php b/flexiapi/app/Http/Controllers/Admin/AccountController.php index 8374346..a3d40bf 100644 --- a/flexiapi/app/Http/Controllers/Admin/AccountController.php +++ b/flexiapi/app/Http/Controllers/Admin/AccountController.php @@ -197,14 +197,6 @@ class AccountController extends Controller return redirect()->route('admin.account.index'); } - public function generateApiKey(Request $request) - { - $account = $request->user(); - $account->generateApiKey(); - - return redirect()->back(); - } - private function fillPassword(Request $request, Account $account) { if ($request->filled('password')) { diff --git a/flexiapi/app/Http/Controllers/Api/AccountController.php b/flexiapi/app/Http/Controllers/Api/AccountController.php index 71356f0..74784fc 100644 --- a/flexiapi/app/Http/Controllers/Api/AccountController.php +++ b/flexiapi/app/Http/Controllers/Api/AccountController.php @@ -23,6 +23,7 @@ use Illuminate\Http\Request; use Illuminate\Validation\Rule; use Illuminate\Support\Str; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Cookie; use App\Http\Controllers\Controller; use Carbon\Carbon; @@ -165,4 +166,15 @@ class AccountController extends Controller return Account::where('id', $request->user()->id) ->delete(); } + + public function generateApiKey(Request $request) + { + $account = $request->user(); + $account->generateApiKey(); + + $account->refresh(); + Cookie::queue('x-api-key', $account->apiKey->key, config('app.api_key_expiration_minutes')); + + return $account->apiKey->key; + } } diff --git a/flexiapi/app/Http/Controllers/Api/MessageController.php b/flexiapi/app/Http/Controllers/Api/MessageController.php new file mode 100644 index 0000000..65efeb2 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Api/MessageController.php @@ -0,0 +1,50 @@ +validate([ + 'to' => 'required', + 'body' => 'required' + ]); + + $returnedLines = []; + + $loop = \React\EventLoop\Loop::get(); + $connector = new \React\Socket\UnixConnector($loop); + + $connector->connect('unix://'.config('app.linphone_daemon_unix_pipe')) + ->then(function (\React\Socket\Connection $connection) use ($request, &$returnedLines) { + + $connection->on('data', function ($message) use ($connection, &$returnedLines) { + foreach (preg_split("/\r\n|\n|\r/", $message) as $line) { + if(!empty($line) && false !== ($matches = explode(':', $line, 2))) { + $returnedLines["{$matches[0]}"] = trim($matches[1]); + } + } + + $connection->close(); + }); + + $connection->write("message sip:".$request->get('to')." ".$request->get('body')."\n"); + }); + + $loop->run(); + + if ($returnedLines['Status'] == 'Error') { + throw ValidationException::withMessages([$returnedLines['Reason']]); + } + + if ($returnedLines['Status'] == 'Ok') { + return response()->json(['id' => $returnedLines['Id']]); + } + } +} diff --git a/flexiapi/app/Http/Kernel.php b/flexiapi/app/Http/Kernel.php index 1281cbf..9790a8f 100644 --- a/flexiapi/app/Http/Kernel.php +++ b/flexiapi/app/Http/Kernel.php @@ -55,7 +55,7 @@ class Kernel extends HttpKernel ], 'api' => [ - 'throttle:600,1', // move to 600 instead of 60 + 'throttle:600,1', // move to 600 instead of 60 'bindings', ], ]; @@ -75,6 +75,8 @@ class Kernel extends HttpKernel 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'cookie' => \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + 'cookie.encrypt' => \App\Http\Middleware\EncryptCookies::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, diff --git a/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php b/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php index 7174650..8f0c951 100644 --- a/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php +++ b/flexiapi/app/Http/Middleware/AuthenticateDigestOrKey.php @@ -21,7 +21,7 @@ namespace App\Http\Middleware; use App\Account; use App\Helpers\Utils; - +use Carbon\Carbon; use Illuminate\Validation\Rule; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Response; @@ -64,9 +64,16 @@ class AuthenticateDigestOrKey } // Key authentication - if ($request->header('x-api-key')) { + + if ($request->header('x-api-key') || $request->cookie('x-api-key')) { if ($account->apiKey - && $account->apiKey->key == $request->header('x-api-key')) { + && ($account->apiKey->key == $request->header('x-api-key') + || $account->apiKey->key == $request->cookie('x-api-key') + )) { + // Refresh the API Key + $account->apiKey->last_used_at = Carbon::now(); + $account->apiKey->save(); + Auth::login($account); $response = $next($request); diff --git a/flexiapi/composer.json b/flexiapi/composer.json index 0c96956..28f875e 100644 --- a/flexiapi/composer.json +++ b/flexiapi/composer.json @@ -17,7 +17,8 @@ "laravel/tinker": "^2.4", "laravelcollective/html": "^6.2", "ovh/ovh": "^2.0", - "parsedown/laravel": "^1.2" + "parsedown/laravel": "^1.2", + "react/socket": "^1.10" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.6", @@ -33,7 +34,10 @@ }, "optimize-autoloader": true, "preferred-install": "dist", - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true + } }, "extra": { "laravel": { diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index 4e8461d..9f7f717 100644 --- a/flexiapi/composer.lock +++ b/flexiapi/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e89db40702fce75cb782f0c1a761aee4", + "content-hash": "9112b0a4903727d599efd72b01596cc4", "packages": [ { "name": "anhskohbo/no-captcha", @@ -996,6 +996,53 @@ }, "time": "2019-12-30T22:54:17+00:00" }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/master" + }, + "time": "2017-07-23T21:35:13+00:00" + }, { "name": "fideloper/proxy", "version": "4.4.1", @@ -3081,6 +3128,539 @@ ], "time": "2021-09-25T23:10:38+00:00" }, + { + "name": "react/cache", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/4bf736a2cccec7298bdf745db77585966fc2ca7e", + "reference": "4bf736a2cccec7298bdf745db77585966fc2ca7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-02-02T06:47:52+00:00" + }, + { + "name": "react/dns", + "version": "v1.9.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "6d38296756fa644e6cb1bfe95eff0f9a4ed6edcb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/6d38296756fa644e6cb1bfe95eff0f9a4ed6edcb", + "reference": "6d38296756fa644e6cb1bfe95eff0f9a4ed6edcb", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.0 || ^2.7 || ^1.2.1", + "react/promise-timer": "^1.8" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^9.3 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.9.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-12-20T08:46:54+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/be6dee480fc4692cec0504e65eb486e3be1aa6f2", + "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop", + "ext-uv": "* for ExtUvLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-07-11T12:31:24+00:00" + }, + { + "name": "react/promise", + "version": "v2.8.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "reference": "f3cff96a19736714524ca0dd1d4130de73dbbbc4", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.5 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v2.8.0" + }, + "time": "2020-05-12T15:16:56+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.8.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/0bbbcc79589e5bfdddba68a287f1cb805581a479", + "reference": "0bbbcc79589e5bfdddba68a287f1cb805581a479", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.0 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Promise\\Timer\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.8.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-12-06T11:08:48+00:00" + }, + { + "name": "react/socket", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "d132fde589ea97f4165f2d94b5296499eac125ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/d132fde589ea97f4165f2d94b5296499eac125ec", + "reference": "d132fde589ea97f4165f2d94b5296499eac125ec", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.8", + "react/event-loop": "^1.2", + "react/promise": "^2.6.0 || ^1.2.1", + "react/promise-timer": "^1.4.0", + "react/stream": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.2", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/promise-stream": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-11-29T10:08:24+00:00" + }, + { + "name": "react/stream", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/7a423506ee1903e89f1e08ec5f0ed430ff784ae9", + "reference": "7a423506ee1903e89f1e08ec5f0ed430ff784ae9", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2021-07-11T12:37:55+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.3.0", diff --git a/flexiapi/composer.phar b/flexiapi/composer.phar index 0cd534bfee653d62a4d99d3cd0c21e4fd3ed8f0b..ae0ea7bbcb361dee588a0cb3ff7d323dcc23f2ec 100755 GIT binary patch delta 13128 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 S>uiS~Xy{*URwhJ+>j#)rd+ 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 env('APP_NAME', 'Laravel'), 'sip_domain' => env('APP_SIP_DOMAIN', 'sip.domain.com'), - 'flexisip_proxy_pid' => env('APP_FLEXISIP_PROXY_PID', '/var/run/flexisip-proxy.pid'), - 'flexisip_pusher_path' => env('APP_FLEXISIP_PUSHER_PATH', ''), 'terms_of_use_url' => env('TERMS_OF_USE_URL', ''), 'privacy_policy_url' => env('PRIVACY_POLICY_URL', ''), @@ -30,6 +28,17 @@ return [ 'proxy_registrar_address' => env('ACCOUNT_PROXY_REGISTRAR_ADDRESS', 'sip.domain.com'), 'transport_protocol_text' => env('ACCOUNT_TRANSPORT_PROTOCOL_TEXT', 'TLS (recommended), TCP or UDP'), + /** + * Time limit before the API Key and related cookie are expired + */ + 'api_key_expiration_minutes' => env('APP_API_KEY_EXPIRATION_MINUTES', 60), + + /** + * External interfaces + */ + 'flexisip_proxy_pid' => env('APP_FLEXISIP_PROXY_PID', '/var/run/flexisip-proxy.pid'), + 'flexisip_pusher_path' => env('APP_FLEXISIP_PUSHER_PATH', ''), + 'linphone_daemon_unix_pipe' => env('APP_LINPHONE_DAEMON_UNIX_PATH', null), /** * Account provisioning diff --git a/flexiapi/database/migrations/2022_02_07_111109_add_last_used_at_column_to_api_keys_table.php b/flexiapi/database/migrations/2022_02_07_111109_add_last_used_at_column_to_api_keys_table.php new file mode 100644 index 0000000..0d62aa6 --- /dev/null +++ b/flexiapi/database/migrations/2022_02_07_111109_add_last_used_at_column_to_api_keys_table.php @@ -0,0 +1,31 @@ +dateTime('last_used_at')->default(''); + } else { + $table->dateTime('last_used_at'); + } + }); + } + + public function down() + { + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('last_used_at'); + }); + } +} diff --git a/flexiapi/phpunit.xml b/flexiapi/phpunit.xml index 1a9e9f6..2c8e571 100644 --- a/flexiapi/phpunit.xml +++ b/flexiapi/phpunit.xml @@ -11,6 +11,8 @@ ./tests/Feature + + ./tests/Feature/AccountMessageTest.php diff --git a/flexiapi/resources/views/account/panel.blade.php b/flexiapi/resources/views/account/panel.blade.php index acc4027..257e8ff 100644 --- a/flexiapi/resources/views/account/panel.blade.php +++ b/flexiapi/resources/views/account/panel.blade.php @@ -67,25 +67,6 @@

Show some registration statistics

- -
API Key
- -

As an administrator you can generate an API key and use it to request the different API endpoints, check the related API documentation to know how to use that key.

- - {!! Form::open(['route' => 'admin.api_key.generate']) !!} -
-
- apiKey) - value="{{ $account->apiKey->key }}" - @endif - > -
-
- -
-
-{!! Form::close() !!} @endif

Account information

@@ -103,6 +84,25 @@ @endif +

API Key

+ +

You can generate an API key and use it to request the different API endpoints, check the related API documentation to know how to use that key.

+ +{!! Form::open(['route' => 'account.api_key.generate']) !!} +
+
+ apiKey) + value="{{ $account->apiKey->key }}" + @endif + > +
+
+ +
+
+{!! Form::close() !!} + @include('parts.account_variables', ['account' => $account]) @endsection \ No newline at end of file diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php index a93b8c6..a83c435 100644 --- a/flexiapi/resources/views/api/documentation_markdown.blade.php +++ b/flexiapi/resources/views/api/documentation_markdown.blade.php @@ -19,8 +19,7 @@ Restricted endpoints are protected using a DIGEST authentication or an API Key m ## Using the API Key -To authenticate using an API Key, you need to @if (config('app.web_panel')) [authenticate to your account panel]({{ route('account.login') }}) @else authenticate to your account panel @endif and be an administrator. -On your panel you will then find a form to generate your personnal key. +You can retrieve an API Key from @if (config('app.web_panel')) [your account panel]({{ route('account.login') }}) @else your account panel @endif or using the dedicated API endpoint. You can then use your freshly generated key by adding a new `x-api-key` header to your API requests: @@ -31,6 +30,15 @@ You can then use your freshly generated key by adding a new `x-api-key` header t > … ``` +Or using a cookie: + +``` +> GET /api/{endpoint} +> from: sip:foobar@sip.example.org +> Cookie: x-api-key={your-api-key} +> … +``` + ## Using DIGEST To discover the available hashing algorythm you MUST send an unauthenticated request to one of the restricted endpoints.
@@ -107,6 +115,10 @@ Those endpoints are authenticated and requires an activated account. ### Accounts +#### `GET /accounts/me/api_key` +Generate and retrieve a fresh API Key. +This endpoint is also setting the API Key as a Cookie. + #### `GET /accounts/me` Retrieve the account information. @@ -272,6 +284,16 @@ Add a type to the account. #### `DELETE /accounts/{id}/contacts/{type_id}` Remove a a type from the account. +### Messages + +#### `POST /messages` +Send a message over SIP. + +JSON parameters: + +* `to` required, SIP address of the receiver +* `body` required, content of the message + ### Statistics #### `GET /statistics/day` diff --git a/flexiapi/routes/api.php b/flexiapi/routes/api.php index 7a0a824..b821dac 100644 --- a/flexiapi/routes/api.php +++ b/flexiapi/routes/api.php @@ -37,6 +37,8 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::get('statistic/week', 'Api\StatisticController@week'); Route::get('statistic/day', 'Api\StatisticController@day'); + Route::get('accounts/me/api_key', 'Api\AccountController@generateApiKey')->middleware('cookie', 'cookie.encrypt'); + Route::get('accounts/me', 'Api\AccountController@show'); Route::delete('accounts/me', 'Api\AccountController@delete'); @@ -53,6 +55,10 @@ Route::group(['middleware' => ['auth.digest_or_key']], function () { Route::get('accounts/me/contacts', 'Api\AccountContactController@index'); Route::group(['middleware' => ['auth.admin']], function () { + if (!empty(config('app.linphone_daemon_unix_pipe'))) { + Route::post('messages', 'Api\MessageController@send'); + } + // Accounts Route::get('accounts/{id}/activate', 'Api\Admin\AccountController@activate'); Route::get('accounts/{id}/deactivate', 'Api\Admin\AccountController@deactivate'); diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index 746dd15..9a59f02 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -61,6 +61,8 @@ if (config('app.web_panel')) { Route::get('panel', 'Account\AccountController@panel')->name('account.panel'); Route::get('logout', 'Account\AuthenticateController@logout')->name('account.logout'); + Route::post('api_key', 'Account\AccountController@generateApiKey')->name('account.api_key.generate'); + Route::get('delete', 'Account\AccountController@delete')->name('account.delete'); Route::delete('delete', 'Account\AccountController@destroy')->name('account.destroy'); @@ -76,8 +78,7 @@ if (config('app.web_panel')) { }); Route::group(['middleware' => 'auth.admin'], function () { - Route::post('admin/api_key', 'Admin\AccountController@generateApiKey')->name('admin.api_key.generate'); - + // Statistics Route::get('admin/statistics/day', 'Admin\StatisticsController@showDay')->name('admin.statistics.show.day'); Route::get('admin/statistics/week', 'Admin\StatisticsController@showWeek')->name('admin.statistics.show.week'); Route::get('admin/statistics/month', 'Admin\StatisticsController@showMonth')->name('admin.statistics.show.month'); diff --git a/flexiapi/tests/Feature/AccountApiKeyTest.php b/flexiapi/tests/Feature/AccountApiKeyTest.php new file mode 100644 index 0000000..96be79b --- /dev/null +++ b/flexiapi/tests/Feature/AccountApiKeyTest.php @@ -0,0 +1,59 @@ +. +*/ + +namespace Tests\Feature; + +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +use App\Password; + +class AccountApiKeyTest extends TestCase +{ + use RefreshDatabase; + + protected $route = '/api/accounts/me/api_key'; + protected $method = 'GET'; + + public function testRefresh() + { + $password = Password::factory()->create(); + + $response0 = $this->generateFirstResponse($password); + $response1 = $this->generateSecondResponse($password, $response0) + ->get($this->route); + + // Get the API Key using the DIGEST method + $password->account->refresh(); + + $response1->assertStatus(200) + ->assertSee($password->account->apiKey->key) + ->assertPlainCookie('x-api-key', $password->account->apiKey->key); + + // Get it again using the key authenticated method + $response2 = $this->keyAuthenticated($password->account) + ->get($this->route); + + $password->account->refresh(); + + $response2->assertStatus(200) + ->assertSee($password->account->apiKey->key) + ->assertPlainCookie('x-api-key', $password->account->apiKey->key); + } +} \ No newline at end of file diff --git a/flexiapi/tests/Feature/AccountApiTest.php b/flexiapi/tests/Feature/AccountApiTest.php index 6a68ce7..4edd726 100644 --- a/flexiapi/tests/Feature/AccountApiTest.php +++ b/flexiapi/tests/Feature/AccountApiTest.php @@ -27,7 +27,6 @@ use App\Admin; use Carbon\Carbon; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Testing\Fluent\AssertableJson; use Tests\TestCase; diff --git a/flexiapi/tests/Feature/AccountContactsTest.php b/flexiapi/tests/Feature/AccountContactsTest.php index 2236588..72eb1c7 100644 --- a/flexiapi/tests/Feature/AccountContactsTest.php +++ b/flexiapi/tests/Feature/AccountContactsTest.php @@ -20,7 +20,6 @@ namespace Tests\Feature; use App\Password; -use App\AccountAction; use App\AccountType; use App\Admin; diff --git a/flexiapi/tests/Feature/AccountMessageTest.php b/flexiapi/tests/Feature/AccountMessageTest.php new file mode 100644 index 0000000..1b3db7a --- /dev/null +++ b/flexiapi/tests/Feature/AccountMessageTest.php @@ -0,0 +1,58 @@ +. +*/ + +namespace Tests\Feature; + +use App\Admin; + +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Testing\Fluent\AssertableJson; +use Tests\TestCase; + +class AccountMessageTest extends TestCase +{ + use RefreshDatabase; + + protected $route = '/api/messages'; + protected $method = 'POST'; + + public function testRequest() + { + $admin = Admin::factory()->create(); + $password = $admin->account->passwords()->first(); + $password->account->generateApiKey(); + + $this->keyAuthenticated($password->account) + ->json($this->method, $this->route, [ + 'to' => '+badid', + 'body' => 'foobar' + ]) + ->assertStatus(422); + + $this->keyAuthenticated($password->account) + ->json($this->method, $this->route, [ + 'to' => 'username@sip.linphone.org', + 'body' => 'Message content' + ]) + ->assertStatus(200) + ->assertJson(function (AssertableJson $json) { + $json->has('id'); + }); + } +} \ No newline at end of file diff --git a/flexiapi/tests/Feature/AccountPhoneChangeTest.php b/flexiapi/tests/Feature/AccountPhoneChangeTest.php index f190f94..039225e 100644 --- a/flexiapi/tests/Feature/AccountPhoneChangeTest.php +++ b/flexiapi/tests/Feature/AccountPhoneChangeTest.php @@ -20,8 +20,6 @@ namespace Tests\Feature; use App\Password; -use App\Account; -use App\Admin; use App\PhoneChangeCode; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -48,7 +46,7 @@ class AccountPhoneChangeTest extends TestCase // Send a SMS /*$this->keyAuthenticated($password->account) ->json($this->method, $this->route.'/request', [ - 'phone' => '+33667545663' + 'phone' => '+3312345678' ]) ->assertStatus(200);*/ } diff --git a/flexiapi/tests/Feature/AccountProvisioningTest.php b/flexiapi/tests/Feature/AccountProvisioningTest.php index f808b6b..8d6e49a 100644 --- a/flexiapi/tests/Feature/AccountProvisioningTest.php +++ b/flexiapi/tests/Feature/AccountProvisioningTest.php @@ -19,7 +19,6 @@ namespace Tests\Feature; -use Account; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; diff --git a/flexisip-account-manager.spec b/flexisip-account-manager.spec index 1b8ef61..787f2c9 100644 --- a/flexisip-account-manager.spec +++ b/flexisip-account-manager.spec @@ -8,7 +8,7 @@ #%define _datadir %{_datarootdir} #%define _docdir %{_datadir}/doc -%define build_number 129 +%define build_number 130 %define var_dir /var/opt/belledonne-communications %define opt_dir /opt/belledonne-communications/share/flexisip-account-manager