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 0cd534b..ae0ea7b 100755 Binary files a/flexiapi/composer.phar and b/flexiapi/composer.phar differ diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php index 88017e5..2fd4596 100644 --- a/flexiapi/config/app.php +++ b/flexiapi/config/app.php @@ -15,8 +15,6 @@ return [ 'name' => 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