Add a provisioning endpoint

Add QRCode link endpoint
Install endroid/qr-code to generate the QRCode
Add a ACCOUNT_PROVISIONING_RC_FILE to configure the provisioning RC file
Complete the documentation
Handle expired confirmation_key in the provisioning endpoints
Implement the provisioning hooks and complete the README
Complete the README regarding the db:import command
Bump the package number
This commit is contained in:
Timothée Jaussoin 2021-06-24 10:22:37 +02:00
parent 88c129dc2c
commit 2062d0618f
10 changed files with 468 additions and 9 deletions

View file

@ -13,6 +13,10 @@ ACCOUNT_PROXY_REGISTRAR_ADDRESS=sip.example.com # Proxy registrar address, can b
ACCOUNT_TRANSPORT_PROTOCOL_TEXT="TLS (recommended), TCP or UDP" # Simple text, to explain how the SIP server can be reached
ACCOUNT_REALM=null # Default realm for the accounts, fallback to the domain if not set, enforce null by default
# Account provisioning
ACCOUNT_PROVISIONING_RC_FILE=
ACCOUNT_PROVISIONING_OVERWRITE_ALL=
# Instance specific parameters
INSTANCE_COPYRIGHT= # Simple text displayed in the page footer
INSTANCE_INTRO_REGISTRATION= # Markdown text displayed in the home page

View file

@ -107,6 +107,14 @@ The `/api` page contains all the required documentation to authenticate and requ
FlexiAPI is shipped with several console commands that you can launch using the `artisan` executable available at the root of this project.
### Migrate an old database
FlexiAPI needs an empty database to run its migration. The following console command allow you to import simultanously an exisiting FlexiSIP database and the old FlexiAPI SQLite database file in the new one. To do so, please specify the new database configuration in the `.env` file and run the following command.
php artisan db:import {old_dbname} {old_sqlite_file_path} --username={old_username} --password={old_password}
You can also specify the `port`, `host` and `database type` as a parameter.
### Clear Expired Nonces for DIGEST authentication
This will remove the nonces stored that were not updated after `x minutes`.
@ -128,3 +136,14 @@ This command will set the admin role to any available FlexiSIP account (the exte
php artisan accounts:set-admin {account_id}
Once one account is declared as administrator, you can directly configure the other ones using the web panel.
## Provisioning
FlexiAPI is providing endpoints to provision Liblinphone powered devices. You can find more documentation about it on the `/api#provisioning` documentation page.
### Provisioning hooks
The XML returned by the provisioning endpoint can be completed using hooks.
To do so, copy and rename the `provisioning_hooks.php.example` file into `provisioning_hooks.php` and complete the functions in the file.
The functions already contains example codes to show you how the XML can be enhanced or completed.

View file

@ -0,0 +1,169 @@
<?php
namespace App\Http\Controllers\Account;
use App\Account;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\Writer\PngWriter;
class ProvisioningController extends Controller
{
public function qrcode(Request $request, $confirmationKey)
{
$account = Account::withoutGlobalScopes()
->where('confirmation_key', $confirmationKey)
->firstOrFail();
if ($account->activationExpired()) abort(404);
$result = Builder::create()
->writer(new PngWriter())
->data(route('provisioning.show', ['confirmation' => $confirmationKey]))
->encoding(new Encoding('UTF-8'))
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
->size(300)
->margin(10)
->build();
return response($result->getString())->header('Content-Type', $result->getMimeType());
}
public function show(Request $request, $confirmationKey = null)
{
// Load the hooks if they exists
$provisioningHooks = config_path('provisioning_hooks.php');
if (file_exists($provisioningHooks)) {
require($provisioningHooks);
}
$dom = new \DOMDocument('1.0', 'UTF-8');
$config = $dom->createElement('config');
$config->setAttribute('xmlns', 'http://www.linphone.org/xsds/lpconfig.xsd');
//$config->setAttribute('xsi:schemaLocation', 'http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd');
//$config->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$dom->appendChild($config);
// Default RC file handling
$rcFile = config('app.provisioning_rc_file');
$proxyConfigIndex = 0;
$authInfoIndex = 0;
if (file_exists($rcFile)) {
$rc = parse_ini_file($rcFile, true);
foreach ($rc as $sectionName => $values) {
$section = $dom->createElement('section');
$section->setAttribute('name', $sectionName);
if (Str::startsWith($sectionName, "proxy_config_")) {
$proxyConfigIndex++;
} elseif (Str::startsWith($sectionName, "auth_info_")) {
$authInfoIndex++;
}
foreach ($values as $key => $value) {
$entry = $dom->createElement('entry', $value);
$entry->setAttribute('name', $key);
$section->appendChild($entry);
}
$config->appendChild($section);
}
}
$account = null;
// Account handling
if ($confirmationKey) {
$account = Account::withoutGlobalScopes()
->where('confirmation_key', $confirmationKey)
->first();
if ($account && !$account->activationExpired()) {
$section = $dom->createElement('section');
$section->setAttribute('name', 'proxy_' . $proxyConfigIndex);
$entry = $dom->createElement('entry', $account->identifier);
$entry->setAttribute('name', 'reg_identity');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 1);
$entry->setAttribute('name', 'reg_sendregister');
$section->appendChild($entry);
$entry = $dom->createElement('entry', 'push_notification');
$entry->setAttribute('name', 'refkey');
$section->appendChild($entry);
// Complete the section with the Proxy hook
if (function_exists('provisioningProxyHook')) {
provisioningProxyHook($section, $request, $account);
}
$config->appendChild($section);
$passwords = $account->passwords()->get();
foreach ($passwords as $password) { // => foreach ($passwords)
$section = $dom->createElement('section');
$section->setAttribute('name', 'auth_info_' . $authInfoIndex);
$entry = $dom->createElement('entry', $account->identifier);
$entry->setAttribute('name', 'username');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $password->password);
$entry->setAttribute('name', 'ha1');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $account->resolvedRealm);
$entry->setAttribute('name', 'realm');
$section->appendChild($entry);
$entry = $dom->createElement('entry', $password->algorithm);
$entry->setAttribute('name', 'algorithm');
$section->appendChild($entry);
// Complete the section with the Auth hook
if (function_exists('provisioningAuthHook')) {
provisioningAuthHook($section, $request, $password);
}
$config->appendChild($section);
$authInfoIndex++;
}
$account->confirmation_key = null;
$account->save();
}
}
// Complete the section with the Auth hook
if (function_exists('provisioningAdditionalSectionHook')) {
provisioningAdditionalSectionHook($config, $request, $account);
}
// Overwrite all the entries
if (config('app.provisioning_overwrite_all')) {
$xpath = new \DOMXpath($dom);
$entries = $xpath->query("//section/entry");
if (!is_null($entries)) {
foreach ($entries as $entry) {
$entry->setAttribute('overwrite', 'true');
}
}
}
return response($dom->saveXML($dom->documentElement))->header('Content-Type', 'application/xml');
}
}

View file

@ -10,6 +10,7 @@
"require": {
"php": "^7.3",
"anhskohbo/no-captcha": "^3.3",
"endroid/qr-code": "^4.1",
"fideloper/proxy": "^4.4",
"laravel/framework": "^8.0",
"laravel/tinker": "^2.4",

174
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": "dba36fc99b2f9684b5127d8511ec640b",
"content-hash": "ed070f83583e71617c9940d4cf7c08a3",
"packages": [
{
"name": "anhskohbo/no-captcha",
@ -70,6 +70,59 @@
},
"time": "2020-09-10T02:31:52+00:00"
},
{
"name": "bacon/bacon-qr-code",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "f73543ac4e1def05f1a70bcd1525c8a157a1ad09"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/f73543ac4e1def05f1a70bcd1525c8a157a1ad09",
"reference": "f73543ac4e1def05f1a70bcd1525c8a157a1ad09",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phly/keep-a-changelog": "^1.4",
"phpunit/phpunit": "^7 | ^8 | ^9",
"squizlabs/php_codesniffer": "^3.4"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.4"
},
"time": "2021-06-18T13:26:35+00:00"
},
{
"name": "brick/math",
"version": "0.9.2",
@ -126,6 +179,53 @@
],
"time": "2021-01-20T22:51:39+00:00"
},
{
"name": "dasprid/enum",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/5abf82f213618696dda8e3bf6f64dd042d8542b2",
"reference": "5abf82f213618696dda8e3bf6f64dd042d8542b2",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
"squizlabs/php_codesniffer": "^3.4"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.3"
},
"time": "2020-10-02T16:03:48+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.0.3",
@ -430,6 +530,78 @@
],
"time": "2020-12-29T14:50:06+00:00"
},
{
"name": "endroid/qr-code",
"version": "4.2.0",
"source": {
"type": "git",
"url": "https://github.com/endroid/qr-code.git",
"reference": "d6d964bda88ea9e40032018208b073845cbd3c2e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/endroid/qr-code/zipball/d6d964bda88ea9e40032018208b073845cbd3c2e",
"reference": "d6d964bda88ea9e40032018208b073845cbd3c2e",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0",
"php": "^7.3||^8.0"
},
"require-dev": {
"endroid/quality": "dev-master",
"ext-gd": "*",
"khanamiryan/qrcode-detector-decoder": "^1.0.4",
"setasign/fpdf": "^1.8.2"
},
"suggest": {
"ext-gd": "Enables you to write PNG images",
"khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
"setasign/fpdf": "Enables you to use the PDF writer"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
}
},
"autoload": {
"psr-4": {
"Endroid\\QrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jeroen van den Enden",
"email": "info@endroid.nl"
}
],
"description": "Endroid QR Code",
"homepage": "https://github.com/endroid/qr-code",
"keywords": [
"code",
"endroid",
"php",
"qr",
"qrcode"
],
"support": {
"issues": "https://github.com/endroid/qr-code/issues",
"source": "https://github.com/endroid/qr-code/tree/4.2.0"
},
"funding": [
{
"url": "https://github.com/endroid",
"type": "github"
}
],
"time": "2021-06-27T06:44:36+00:00"
},
{
"name": "erusev/parsedown",
"version": "1.7.4",

View file

@ -28,6 +28,12 @@ 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'),
/**
* Account provisioning
*/
'provisioning_rc_file' => env('ACCOUNT_PROVISIONING_RC_FILE', ''),
'provisioning_overwrite_all' => env('ACCOUNT_PROVISIONING_OVERWRITE_ALL', false),
/**
* Set a global realm for all the accounts, if not set, the account domain
* will be used as a fallback

View file

@ -0,0 +1,72 @@
<?php
use App\Account;
use App\Password;
use Illuminate\Http\Request;
/**
* This file contains hooks functions used by the provisioning query
* Check the commented code to have an overview of what can be done using the parameters
*/
/**
* @brief Complete the proxy section XML node
* @param DOMElement $proxySection
* @param Request $request
* @param Account $account
* @return void
*/
function provisioningProxyHook(\DOMElement $proxySection, Request $request, Account $account)
{
/*
// Transfort get parameters from the URI into entries
foreach ($request->all() as $parameterKey => $parameterValue) {
$entry = $proxySection->ownerDocument->createElement('entry', $parameterValue);
$entry->setAttribute('name', $parameterKey);
// Overwrite an existing value
$entry->setAttribute('overwrite', 'true');
$proxySection->appendChild($entry);
}
*/
}
/**
* @brief Complete a Auth section XML node
* @param DOMElement $proxySection
* @param Request $request
* @param Password $password
* @return void
*/
function provisioningAuthHook(\DOMElement $authSection, Request $request, Password $password)
{
/*
// Inject the related account domain into the request
$entry = $authSection->ownerDocument->createElement('entry', $password->account->domain);
$entry->setAttribute('name', 'domain');
$authSection->appendChild($entry);
*/
}
/**
* @brief Complete the proxy section XML node, the Account might be passed as a parameter if resolved
* @param DOMElement $proxySection
* @param Request $request
* @param Account $account
* @return void
*/
function provisioningAdditionalSectionHook(\DOMElement $config, Request $request, ?Account $account)
{
/*
// Add another section
$section = $config->ownerDocument->createElement('section');
$section->setAttribute('name', 'new_section');
$entry = $config->ownerDocument->createElement('entry', 'entry_value');
$entry->setAttribute('name', 'entry_key');
$section->appendChild($entry);
$config->appendChild($section);
*/
}

View file

@ -13,11 +13,11 @@
> content-type: application/json
> accept: application/json</code></pre>
<h2>Authentication</h2>
<h2 id="authentication"><a href="#authentication">Authentication</a></h2>
<p>Restricted endpoints are protected using a DIGEST authentication or an API Key mechanisms.</p>
<h3>Using the API Key</h3>
<h3 id="authentication_api_key"><a href="#authentication_api_key">Using the API Key</a></h3>
<p>To authenticate using an API Key, you need to <a href="{{ route('account.login') }}">authenticate to your account panel</a> and being an administrator.</p>
<p>On your panel you will then find a form to generate your personnal key.</p>
@ -30,7 +30,7 @@
> x-api-key: {your-api-key}
> </code></pre>
<h3>Using DIGEST</h3>
<h3 id="authentication_digest"><a href="#authentication_digest">Using DIGEST</a></h3>
<p>To discover the available hashing algorythm you MUST send an unauthenticated request to one of the restricted endpoints.<br />
For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the authentication layer.</p>
@ -46,9 +46,9 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<p>You can find more documentation on the related <a href="https://tools.ietf.org/html/rfc7616">IETF RFC-7616</a>.</p>
<h2>Endpoints</h2>
<h2 id="endpoints"><a href="#endpoints">Endpoints</a></h2>
<h3>Public endpoints</h3>
<h3 id="public_endpoints"><a href="#public_endpoints">Public endpoints</a></h3>
<h4><code>GET /ping</code></h4>
<p>Returns <code>pong</code></p>
@ -100,7 +100,7 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<li><code>code</code> the PIN code</li>
</ul>
<h3>User authenticated endpoints</h3>
<h3 id="authenticated_endpoints"><a href="#authenticated_endpoints">User authenticated endpoints</a></h3>
<p>Those endpoints are authenticated and requires an activated account.</p>
<h4><code>GET /accounts/me</code></h4>
@ -151,7 +151,7 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<h4><code>DELETE /accounts/me/devices/{uuid}</code></h4>
<p>Remove one of the user registered devices.</p>
<h3>Admin endpoints</h3>
<h3 id="admin_endpoints"><a href="#admin_endpoints">Admin endpoints</a></h3>
<p>Those endpoints are authenticated and requires an admin account.</p>
@ -186,4 +186,17 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
<h4><code>GET /accounts/{id}/deactivate</code></h4>
<p>Deactivate an account.</p>
<h2 id="provisioning"><a href="#provisioning">Provisioning</a></h2>
<p>When an account is having an available <code>confirmation_key</code> it can be provisioned using the two following URL.</p>
<p>Those two URL are <b>not API endpoints</b>, they are not located under <code>/api</code>.
<h4><code>VISIT /provisioning/{confirmation_key}</code></h4>
<p>Return the provisioning information available in the liblinphone configuration file (if correctly configured).</p>
<p>If the <code>confirmation_key</code> is valid the related account information are added to the returned XML. The account is then considered as "provisioned" and those account related information will be removed in the upcoming requests.</p>
<h4><code>VISIT /provisioning/qrcode/{confirmation_key}</code></h4>
<p>Return a QRCode that points to the provisioning URL.</p>
@endsection

View file

@ -33,6 +33,9 @@ Route::post('authenticate/phone/confirm', 'Account\AuthenticateController@valida
Route::get('register', 'Account\RegisterController@register')->name('account.register');
Route::get('provisioning/qrcode/{confirmation}', 'Account\ProvisioningController@qrcode')->name('provisioning.qrcode');
Route::get('provisioning/{confirmation?}', 'Account\ProvisioningController@show')->name('provisioning.show');
if (config('app.phone_authentication')) {
Route::get('register/phone', 'Account\RegisterController@registerPhone')->name('account.register.phone');
Route::post('register/phone', 'Account\RegisterController@storePhone')->name('account.store.phone');

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 83
%define build_number 84
%define var_dir /var/opt/belledonne-communications
%define opt_dir /opt/belledonne-communications/share/flexisip-account-manager