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
This commit is contained in:
Timothée Jaussoin 2022-01-11 16:07:55 +01:00
parent a1780254d7
commit 20f8fb4c45
30 changed files with 954 additions and 53 deletions

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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();
}

View file

@ -0,0 +1,49 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
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
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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');
}
}

View file

@ -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()
{

View file

@ -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', [

View file

@ -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')) {

View file

@ -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;
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class MessageController extends Controller
{
public function send(Request $request)
{
$request->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']]);
}
}
}

View file

@ -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,

View file

@ -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);

View file

@ -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": {

582
flexiapi/composer.lock generated
View file

@ -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",

Binary file not shown.

View file

@ -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

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
use App\ApiKey;
class AddLastUsedAtColumnToApiKeysTable extends Migration
{
public function up()
{
ApiKey::truncate();
Schema::table('api_keys', function (Blueprint $table) {
if (DB::getDriverName() == 'sqlite') {
$table->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');
});
}
}

View file

@ -11,6 +11,8 @@
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
<!-- Exclude the Message test suite as it relies on the linphone-daemon that can't be mocked for the moment -->
<exclude>./tests/Feature/AccountMessageTest.php</exclude>
</testsuite>
</testsuites>
<php>

View file

@ -67,25 +67,6 @@
<p class="mb-1">Show some registration statistics</p>
</a>
</div>
<h5>API Key</h5>
<p>As an administrator you can generate an API key and use it to request the different API endpoints, <a href="{{ route('api') }}">check the related API documentation</a> to know how to use that key.</p>
{!! Form::open(['route' => 'admin.api_key.generate']) !!}
<div class="form-row">
<div class="col-8">
<input readonly class="form-control" placeholder="No key yet, press Generate"
@if ($account->apiKey)
value="{{ $account->apiKey->key }}"
@endif
>
</div>
<div class="col-4">
<button type="submit" class="btn btn-primary">Generate</button>
</div>
</div>
{!! Form::close() !!}
@endif
<h3 class="mt-3">Account information</h3>
@ -103,6 +84,25 @@
@endif
</div>
<h3 class="mt-3">API Key</h3>
<p>You can generate an API key and use it to request the different API endpoints, <a href="{{ route('api') }}">check the related API documentation</a> to know how to use that key.</p>
{!! Form::open(['route' => 'account.api_key.generate']) !!}
<div class="form-row">
<div class="col-8">
<input readonly class="form-control" placeholder="No key yet, press Generate"
@if ($account->apiKey)
value="{{ $account->apiKey->key }}"
@endif
>
</div>
<div class="col-4">
<button type="submit" class="btn btn-primary">Generate</button>
</div>
</div>
{!! Form::close() !!}
@include('parts.account_variables', ['account' => $account])
@endsection

View file

@ -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 <a href="#get-accountsmeapikey">the dedicated API endpoint</a>.
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.<br />
@ -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`

View file

@ -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');

View file

@ -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');

View file

@ -0,0 +1,59 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
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
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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);
}
}

View file

@ -27,7 +27,6 @@ use App\Admin;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\Fluent\AssertableJson;
use Tests\TestCase;

View file

@ -20,7 +20,6 @@
namespace Tests\Feature;
use App\Password;
use App\AccountAction;
use App\AccountType;
use App\Admin;

View file

@ -0,0 +1,58 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2022 Belledonne Communications SARL, All rights reserved.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
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
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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');
});
}
}

View file

@ -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);*/
}

View file

@ -19,7 +19,6 @@
namespace Tests\Feature;
use Account;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

View file

@ -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