From 2062d0618f8e8ae2e9c1b121cc66b3fc6a706f93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Jaussoin?= Date: Thu, 24 Jun 2021 10:22:37 +0200 Subject: [PATCH] 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 --- flexiapi/.env.example | 4 + flexiapi/README.md | 19 ++ .../Account/ProvisioningController.php | 169 +++++++++++++++++ flexiapi/composer.json | 1 + flexiapi/composer.lock | 174 +++++++++++++++++- flexiapi/config/app.php | 6 + .../config/provisioning_hooks.php.example | 72 ++++++++ .../resources/views/documentation.blade.php | 27 ++- flexiapi/routes/web.php | 3 + flexisip-account-manager.spec | 2 +- 10 files changed, 468 insertions(+), 9 deletions(-) create mode 100644 flexiapi/app/Http/Controllers/Account/ProvisioningController.php create mode 100644 flexiapi/config/provisioning_hooks.php.example diff --git a/flexiapi/.env.example b/flexiapi/.env.example index 4dde25c..2055d68 100644 --- a/flexiapi/.env.example +++ b/flexiapi/.env.example @@ -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 diff --git a/flexiapi/README.md b/flexiapi/README.md index 027a938..d7fea24 100644 --- a/flexiapi/README.md +++ b/flexiapi/README.md @@ -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. \ No newline at end of file diff --git a/flexiapi/app/Http/Controllers/Account/ProvisioningController.php b/flexiapi/app/Http/Controllers/Account/ProvisioningController.php new file mode 100644 index 0000000..fce1811 --- /dev/null +++ b/flexiapi/app/Http/Controllers/Account/ProvisioningController.php @@ -0,0 +1,169 @@ +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'); + } +} diff --git a/flexiapi/composer.json b/flexiapi/composer.json index 9963d78..2383b0f 100644 --- a/flexiapi/composer.json +++ b/flexiapi/composer.json @@ -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", diff --git a/flexiapi/composer.lock b/flexiapi/composer.lock index 192cdef..be26539 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": "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", diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php index ca37c77..6979120 100644 --- a/flexiapi/config/app.php +++ b/flexiapi/config/app.php @@ -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 diff --git a/flexiapi/config/provisioning_hooks.php.example b/flexiapi/config/provisioning_hooks.php.example new file mode 100644 index 0000000..db24e00 --- /dev/null +++ b/flexiapi/config/provisioning_hooks.php.example @@ -0,0 +1,72 @@ +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); + */ +} \ No newline at end of file diff --git a/flexiapi/resources/views/documentation.blade.php b/flexiapi/resources/views/documentation.blade.php index 77ddcec..f1af6a6 100644 --- a/flexiapi/resources/views/documentation.blade.php +++ b/flexiapi/resources/views/documentation.blade.php @@ -13,11 +13,11 @@ > content-type: application/json > accept: application/json -

Authentication

+

Authentication

Restricted endpoints are protected using a DIGEST authentication or an API Key mechanisms.

-

Using the API Key

+

Using the API Key

To authenticate using an API Key, you need to authenticate to your account panel and being an administrator.

On your panel you will then find a form to generate your personnal key.

@@ -30,7 +30,7 @@ > x-api-key: {your-api-key} > … -

Using DIGEST

+

Using DIGEST

To discover the available hashing algorythm you MUST send an unauthenticated request to one of the restricted endpoints.
For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the authentication layer.

@@ -46,9 +46,9 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth

You can find more documentation on the related IETF RFC-7616.

-

Endpoints

+

Endpoints

-

Public endpoints

+

Public endpoints

GET /ping

Returns pong

@@ -100,7 +100,7 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth
  • code the PIN code
  • -

    User authenticated endpoints

    +

    User authenticated endpoints

    Those endpoints are authenticated and requires an activated account.

    GET /accounts/me

    @@ -151,7 +151,7 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth

    DELETE /accounts/me/devices/{uuid}

    Remove one of the user registered devices.

    -

    Admin endpoints

    +

    Admin endpoints

    Those endpoints are authenticated and requires an admin account.

    @@ -186,4 +186,17 @@ For the moment only DIGEST-MD5 and DIGEST-SHA-256 are supported through the auth

    GET /accounts/{id}/deactivate

    Deactivate an account.

    +

    Provisioning

    + +

    When an account is having an available confirmation_key it can be provisioned using the two following URL.

    + +

    Those two URL are not API endpoints, they are not located under /api. + +

    VISIT /provisioning/{confirmation_key}

    +

    Return the provisioning information available in the liblinphone configuration file (if correctly configured).

    +

    If the confirmation_key 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.

    + +

    VISIT /provisioning/qrcode/{confirmation_key}

    +

    Return a QRCode that points to the provisioning URL.

    + @endsection diff --git a/flexiapi/routes/web.php b/flexiapi/routes/web.php index 8d40b43..8a950ae 100644 --- a/flexiapi/routes/web.php +++ b/flexiapi/routes/web.php @@ -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'); diff --git a/flexisip-account-manager.spec b/flexisip-account-manager.spec index 79d157b..14d047c 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 83 +%define build_number 84 %define var_dir /var/opt/belledonne-communications %define opt_dir /opt/belledonne-communications/share/flexisip-account-manager