From 5b2b834269f74865c8ca6ab5e81f6834228490d4 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Tue, 27 Jul 2021 23:24:23 -0400 Subject: build: shepherd: Use autoload to lazily bind Shepherd modules. Instead of imperative module-autoload! directives. * gnu/build/shepherd.scm: Replace module-autoload! directives by autoload arguments for define-module. --- gnu/build/shepherd.scm | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'gnu/build') diff --git a/gnu/build/shepherd.scm b/gnu/build/shepherd.scm index d7b858dea4..778e3fc627 100644 --- a/gnu/build/shepherd.scm +++ b/gnu/build/shepherd.scm @@ -24,6 +24,12 @@ (define-module (gnu build shepherd) #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) #:use-module (ice-9 match) + ;; XXX: Lazy-bind the Shepherd to avoid a compile-time dependency. + #:autoload (shepherd service) (fork+exec-command + read-pid-file + exec-command + %precious-signals) + #:autoload (shepherd system) (unblock-signals) #:export (make-forkexec-constructor/container fork+exec-command/container)) @@ -92,14 +98,6 @@ (define accounts (file-exists? (file-system-mapping-source mapping))) mappings))))) -;; XXX: Lazy-bind the Shepherd to avoid a compile-time dependency. -(module-autoload! (current-module) - '(shepherd service) - '(fork+exec-command read-pid-file exec-command - %precious-signals)) -(module-autoload! (current-module) - '(shepherd system) '(unblock-signals)) - (define* (read-pid-file/container pid pid-file #:key (max-delay 5)) "Read PID-FILE in the container namespaces of PID, which exists in a separate mount and PID name space. Return the \"outer\" PID. " -- cgit v1.2.3 From 69dcc24c9f0cdfea674eb690e7755d26a25ced2b Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Thu, 15 Apr 2021 01:18:24 -0400 Subject: services: Add a service for Jami. * gnu/services/telephony.scm (string-or-computed-file?) (string-list?, account-fingerprint-list?): New procedures. (maybe-string-list, maybe-account-fingerprint-list) (maybe-boolean, maybe-string, jami-account-list): New configuration field types. (serialize-string-list, serialize-boolean, serialize-string) (jami-account, jami-account->alist, jami-configuration) (jami-account-list?, jami-account-list-maybe): New procedures. (%jami-accounts): New variable. (jami-configuration->command-line-arguments): New procedure. (jami-dbus-session-activation, jami-shepherd-services): New procedures. (jami-service-type): New variable. * gnu/build/jami-service.scm: New file. * gnu/tests/data/jami-dummy-account.dat: Likewise. * gnu/tests/telephony.scm: Likewise. * gnu/local.mk (GNU_SYSTEM_MODULES): Register them. * Makefile.am (SCM_TESTS): Register the test file. (dist_patch_DATA): Register the new data file. * doc/guix.texi (Telephony Services): Document it. --- Makefile.am | 1 + doc/guix.texi | 228 ++++++++++++ gnu/build/jami-service.scm | 587 +++++++++++++++++++++++++++++ gnu/local.mk | 5 +- gnu/services/telephony.scm | 684 +++++++++++++++++++++++++++++++++- gnu/tests/data/jami-dummy-account.dat | 392 +++++++++++++++++++ gnu/tests/telephony.scm | 366 ++++++++++++++++++ tests/services/telephony.scm | 446 ++++++++++++++++++++++ 8 files changed, 2706 insertions(+), 3 deletions(-) create mode 100644 gnu/build/jami-service.scm create mode 100644 gnu/tests/data/jami-dummy-account.dat create mode 100644 gnu/tests/telephony.scm create mode 100644 tests/services/telephony.scm (limited to 'gnu/build') diff --git a/Makefile.am b/Makefile.am index d5ec909213..5542aa1c56 100644 --- a/Makefile.am +++ b/Makefile.am @@ -489,6 +489,7 @@ SCM_TESTS = \ tests/services/file-sharing.scm \ tests/services/configuration.scm \ tests/services/linux.scm \ + tests/services/telephony.scm \ tests/sets.scm \ tests/size.scm \ tests/status.scm \ diff --git a/doc/guix.texi b/doc/guix.texi index 2298d512a1..9a9c85678c 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -22526,6 +22526,234 @@ and Error. @node Telephony Services @subsection Telephony Services +@cindex telephony, services +The @code{(gnu services telephony)} module contains Guix service +definitions for telephony services. Currently it provides the following +services: + +@subsubheading Jami + +@cindex jami, service + +This section describes how to configure a Jami server that can be used +to host video (or audio) conferences, among other uses. The following +example demonstrates how to specify Jami account archives (backups) to +be provisioned automatically: + +@lisp +(service jami-service-type + (jami-configuration + (accounts + (list (jami-account + (archive "/etc/jami/unencrypted-account-1.gz")) + (jami-account + (archive "/etc/jami/unencrypted-account-2.gz")))))) +@end lisp + +When the accounts field is specified, the Jami account files of the +service found under @file{/var/lib/jami} are recreated every time the +service starts. + +Jami accounts and their corresponding backup archives can be generated +using either the @code{jami-qt} or @code{jami-gnome} Jami clients. The +accounts should not be password-protected, but it is wise to ensure +their files are only readable by @samp{root}. + +The next example shows how to declare that only some contacts should be +allowed to communicate with a given account: + +@lisp +(service jami-service-type + (jami-configuration + (accounts + (list (jami-account + (archive "/etc/jami/unencrypted-account-1.gz") + (peer-discovery? #t) + (rendezvous-point? #t) + (allowed-contacts + '("1dbcb0f5f37324228235564b79f2b9737e9a008f" + "2dbcb0f5f37324228235564b79f2b9737e9a008f"))))))) +@end lisp + +In this mode, only the declared @code{allowed-contacts} can initiate +communication with the Jami account. This can be used, for example, +with rendezvous point accounts to create a private video conferencing +space. + +To put the system administrator in full control of the conferences +hosted on their system, the Jami service supports the following actions: + +@example sh +# herd doc jami list-actions jami +(list-accounts + list-account-details + list-banned-contacts + list-contacts + list-moderators + add-moderator + ban-contact + enable-account + disable-account) +@end example + +The above actions aim to provide the most valuable actions for +moderation purposes, not to cover the whole Jami API. Users wanting to +interact with the Jami daemon from Guile may be interested in +experimenting with the @code{(gnu build jami-service)} module, which +powers the above Shepherd actions. + +@c TODO: This should be auto-generated from the doc already defined on +@c the shepherd-actions themselves in (gnu services telephony). +The @code{add-moderator} and @code{ban-contact} actions accept a contact +@emph{fingerprint} (40 characters long hash) as first argument and an +account fingerprint or username as second argument: + +@example sh +# herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f \ + f3345f2775ddfe07a4b0d95daea111d15fbc1199 + +# herd list-moderators jami +Moderators for account f3345f2775ddfe07a4b0d95daea111d15fbc1199: + - 1dbcb0f5f37324228235564b79f2b9737e9a008f + +@end example + +In the case of @code{ban-contact}, the second username argument is +optional; when omitted, the account is banned from all Jami accounts: + +@example sh +# herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f + +# herd list-banned-contacts jami +Banned contacts for account f3345f2775ddfe07a4b0d95daea111d15fbc1199: + - 1dbcb0f5f37324228235564b79f2b9737e9a008f + +@end example + +Banned contacts are also stripped from their moderation privileges. + +The @code{disable-account} action allows to completely disconnect an +account from the network, making it unreachable, while +@code{enable-account} does the inverse. They accept a single account +username or fingerprint as first argument: + +@example sh +# herd disable-account jami f3345f2775ddfe07a4b0d95daea111d15fbc1199 + +# herd list-accounts jami +The following Jami accounts are available: + - f3345f2775ddfe07a4b0d95daea111d15fbc1199 (dummy) [disabled] + +@end example + +The @code{list-account-details} action prints the detailed parameters of +each accounts in the Recutils format, which means the @command{recsel} +command can be used to select accounts of interest (@pxref{Selection +Expressions,,,recutils, GNU recutils manual}). Note that period +characters (@samp{.}) found in the account parameter keys are mapped to +underscores (@samp{_}) in the output, to meet the requirements of the +Recutils format. The following example shows how to print the account +fingerprints for all accounts operating in the rendezvous point mode: + +@example sh +# herd list-account-details jami | \ + recsel -p Account.username -e 'Account.rendezVous ~ "true"' +Account_username: f3345f2775ddfe07a4b0d95daea111d15fbc1199 +@end example + +The remaining actions should be self-explanatory. + +The complete set of available configuration options is detailed below. + +@c TODO: Ideally, the following fragments would be auto-generated at +@c build time, so that they needn't be manually duplicated. +@c Auto-generated via (configuration->documentation 'jami-configuration) +@deftp {Data Type} jami-configuration +Available @code{jami-configuration} fields are: + +@table @asis +@item @code{jamid} (default: @code{libring}) (type: package) +The Jami daemon package to use. + +@item @code{dbus} (default: @code{dbus}) (type: package) +The D-Bus package to use to start the required D-Bus session. + +@item @code{nss-certs} (default: @code{nss-certs}) (type: package) +The nss-certs package to use to provide TLS certificates. + +@item @code{enable-logging?} (default: @code{#t}) (type: boolean) +Whether to enable logging to syslog. + +@item @code{debug?} (default: @code{#f}) (type: boolean) +Whether to enable debug level messages. + +@item @code{auto-answer?} (default: @code{#f}) (type: boolean) +Whether to force automatic answer to incoming calls. + +@item @code{accounts} (default: @code{disabled}) (type: maybe-jami-account-list) +A list of Jami accounts to be (re-)provisioned every time the Jami +daemon service starts. When providing this field, the account +directories under @file{/var/lib/jami/} are recreated every time the +service starts, ensuring a consistent state. + +@end table + +@end deftp + +@c Auto-generated via (configuration->documentation 'jami-account) +@deftp {Data Type} jami-account +Available @code{jami-account} fields are: + +@table @asis +@item @code{archive} (type: string-or-computed-file) +The account archive (backup) file name of the account. This is used to +provision the account when the service starts. The account archive +should @emph{not} be encrypted. It is highly recommended to make it +readable only to the @samp{root} user (i.e., not in the store), to guard +against leaking the secret key material of the Jami account it contains. + +@item @code{allowed-contacts} (default: @code{disabled}) (type: maybe-account-fingerprint-list) +The list of allowed contacts for the account, entered as their 40 +characters long fingerprint. Messages or calls from accounts not in +that list will be rejected. When unspecified, the configuration of the +account archive is used as-is with respect to contacts and public +inbound calls/messaging allowance, which typically defaults to allow any +contact to communicate with the account. + +@item @code{moderators} (default: @code{disabled}) (type: maybe-account-fingerprint-list) +The list of contacts that should have moderation privileges (to ban, +mute, etc. other users) in rendezvous conferences, entered as their 40 +characters long fingerprint. When unspecified, the configuration of the +account archive is used as-is with respect to moderation, which +typically defaults to allow anyone to moderate. + +@item @code{rendezvous-point?} (default: @code{disabled}) (type: maybe-boolean) +Whether the account should operate in the rendezvous mode. In this +mode, all the incoming audio/video calls are mixed into a conference. +When left unspecified, the value from the account archive prevails. + +@item @code{peer-discovery?} (default: @code{disabled}) (type: maybe-boolean) +Whether peer discovery should be enabled. Peer discovery is used to +discover other OpenDHT nodes on the local network, which can be useful +to maintain communication between devices on such network even when the +connection to the the Internet has been lost. When left unspecified, +the value from the account archive prevails. + +@item @code{bootstrap-hostnames} (default: @code{disabled}) (type: maybe-string-list) +A list of hostnames or IPs pointing to OpenDHT nodes, that should be +used to initially join the OpenDHT network. When left unspecified, the +value from the account archive prevails. + +@item @code{name-server-uri} (default: @code{disabled}) (type: maybe-string) +The URI of the name server to use, that can be used to retrieve the +account fingerprint for a registered username. + +@end table + +@end deftp + +@subsubheading Murmur (VoIP server) + @cindex Murmur (VoIP server) @cindex VoIP server This section describes how to set up and run a Murmur server. Murmur is diff --git a/gnu/build/jami-service.scm b/gnu/build/jami-service.scm new file mode 100644 index 0000000000..d44e87387d --- /dev/null +++ b/gnu/build/jami-service.scm @@ -0,0 +1,587 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Maxim Cournoyer +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix 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 General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +;;; Commentary: +;;; +;;; This module contains helpers used as part of the jami-service-type +;;; definition. +;;; +;;; Code: + +(define-module (gnu build jami-service) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (ice-9 peg) + #:use-module (ice-9 rdelim) + #:use-module (ice-9 regex) + #:use-module (rnrs io ports) + #:autoload (shepherd service) (fork+exec-command) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:export (account-fingerprint? + account-details->recutil + get-accounts + get-usernames + set-account-details + add-account + account->username + username->account + username->contacts + enable-account + disable-account + + add-contact + remove-contact + + set-all-moderators + set-moderator + username->all-moderators? + username->moderators + + dbus-available-services + dbus-service-available? + + %send-dbus-binary + %send-dbus-bus + %send-dbus-user + %send-dbus-group + %send-dbus-debug + send-dbus + + with-retries)) + +;;; +;;; Utilities. +;;; + +(define-syntax-rule (with-retries n delay body ...) + "Retry the code in BODY up to N times until it doesn't raise an exception +nor return #f, else raise an error. A delay of DELAY seconds is inserted +before each retry." + (let loop ((attempts 0)) + (catch #t + (lambda () + (let ((result (begin body ...))) + (if (not result) + (error "failed attempt" attempts) + result))) + (lambda args + (if (< attempts n) + (begin + (sleep delay) ;else wait and retry + (loop (+ 1 attempts))) + (error "maximum number of retry attempts reached" + body ... args)))))) + +(define (alist->list alist) + "Flatten ALIST into a list." + (append-map (match-lambda + (() '()) + ((key . value) + (list key value))) + alist)) + +(define account-fingerprint-rx (make-regexp "[0-9A-f]{40}")) + +(define (account-fingerprint? val) + "A Jami account fingerprint is 40 characters long and only contains +hexadecimal characters." + (and (string? val) + (regexp-exec account-fingerprint-rx val))) + + +;;; +;;; D-Bus reply parser. +;;; + +(define (parse-dbus-reply reply) + "Return the parse tree of REPLY, a string returned by the 'dbus-send' +command." + ;; Refer to 'man 1 dbus-send' for the grammar reference. Note that the + ;; format of the replies doesn't match the format of the input, which is the + ;; one documented, but it gives an idea. For an even better reference, see + ;; the `print_iter' procedure of the 'dbus-print-message.c' file from the + ;; 'dbus' package sources. + (define-peg-string-patterns + "contents <- header (item / container (item / container*)?) + item <-- WS type WS value NL + container <- array / dict / variant + array <-- array-start (item / container)* array-end + dict <-- array-start dict-entry* array-end + dict-entry <-- dict-entry-start item item dict-entry-end + variant <-- variant-start item + type <-- 'string' / 'int16' / 'uint16' / 'int32' / 'uint32' / 'int64' / + 'uint64' / 'double' / 'byte' / 'boolean' / 'objpath' + value <-- (!NL .)* NL + header < (!NL .)* NL + variant-start < WS 'variant' + array-start < WS 'array [' NL + array-end < WS ']' NL + dict-entry-start < WS 'dict entry(' NL + dict-entry-end < WS ')' NL + DQ < '\"' + WS < ' '* + NL < '\n'*") + + (peg:tree (match-pattern contents reply))) + +(define (strip-quotes text) + "Strip the leading and trailing double quotes (\") characters from TEXT." + (let* ((text* (if (string-prefix? "\"" text) + (string-drop text 1) + text)) + (text** (if (string-suffix? "\"" text*) + (string-drop-right text* 1) + text*))) + text**)) + +(define (deserialize-item item) + "Return the value described by the ITEM parse tree as a Guile object." + ;; Strings are printed wrapped in double quotes (see the print_iter + ;; procedure in dbus-print-message.c). + (match item + (('item ('type "string") ('value value)) + (strip-quotes value)) + (('item ('type "boolean") ('value value)) + (if (string=? "true" value) + #t + #f)) + (('item _ ('value value)) + value))) + +(define (serialize-boolean bool) + "Return the serialized format expected by dbus-send for BOOL." + (format #f "boolean:~:[false~;true~]" bool)) + +(define (dict->alist dict-parse-tree) + "Translate a dict parse tree to an alist." + (define (tuples->alist tuples) + (map (lambda (x) (apply cons x)) tuples)) + + (match dict-parse-tree + ('dict + '()) + (('dict ('dict-entry keys values) ...) + (let ((keys* (map deserialize-item keys)) + (values* (map deserialize-item values))) + (tuples->alist (zip keys* values*)))))) + +(define (array->list array-parse-tree) + "Translate an array parse tree to a list." + (match array-parse-tree + ('array + '()) + (('array items ...) + (map deserialize-item items)))) + + +;;; +;;; Low-level, D-Bus-related procedures. +;;; + +;;; The following parameters are used in the jami-service-type service +;;; definition to conveniently customize the behavior of the send-dbus helper, +;;; even when called indirectly. +(define %send-dbus-binary (make-parameter "dbus-send")) +(define %send-dbus-bus (make-parameter #f)) +(define %send-dbus-user (make-parameter #f)) +(define %send-dbus-group (make-parameter #f)) +(define %send-dbus-debug (make-parameter #f)) + +(define* (send-dbus #:key service path interface method + bus + dbus-send + user group + timeout + arguments) + "Return the response of DBUS-SEND, else raise an error. Unless explicitly +provided, DBUS-SEND takes the value of the %SEND-DBUS-BINARY parameter. BUS +can be used to specify the bus address, such as 'unix:path=/var/run/jami/bus'. +Alternatively, the %SEND-DBUS-BUS parameter can be used. ARGUMENTS can be +used to pass input values to a D-Bus method call. TIMEOUT is the amount of +time to wait for a reply in milliseconds before giving up with an error. USER +and GROUP allow choosing under which user/group the DBUS-SEND command is +executed. Alternatively, the %SEND-DBUS-USER and %SEND-DBUS-GROUP parameters +can be used instead." + (let* ((command `(,(if dbus-send + dbus-send + (%send-dbus-binary)) + ,@(if (or bus (%send-dbus-bus)) + (list (string-append "--bus=" + (or bus (%send-dbus-bus)))) + '()) + "--print-reply" + ,@(if timeout + (list (format #f "--reply-timeout=~d" timeout)) + '()) + ,(string-append "--dest=" service) ;e.g., cx.ring.Ring + ,path ;e.g., /cx/ring/Ring/ConfigurationManager + ,(string-append interface "." method) + ,@(or arguments '()))) + (temp-port (mkstemp! (string-copy "/tmp/dbus-send-output-XXXXXXX"))) + (temp-file (port-filename temp-port))) + (dynamic-wind + (lambda () + (let* ((uid (or (and=> (or user (%send-dbus-user)) + (compose passwd:uid getpwnam)) -1)) + (gid (or (and=> (or group (%send-dbus-group)) + (compose group:gid getgrnam)) -1))) + (chown temp-port uid gid))) + (lambda () + (let ((pid (fork+exec-command command + #:user (or user (%send-dbus-user)) + #:group (or group (%send-dbus-group)) + #:log-file temp-file))) + (match (waitpid pid) + ((_ . status) + (let ((exit-status (status:exit-val status)) + (output (call-with-port temp-port get-string-all))) + (if (= 0 exit-status) + output + (error "the send-dbus command exited with: " + command exit-status output))))))) + (lambda () + (false-if-exception (delete-file temp-file)))))) + +(define (parse-account-ids reply) + "Return the Jami account IDs from REPLY, which is assumed to be the output +of the Jami D-Bus `getAccountList' method." + (array->list (parse-dbus-reply reply))) + +(define (parse-account-details reply) + "Parse REPLY, which is assumed to be the output of the Jami D-Bus +`getAccountDetails' method, and return its content as an alist." + (dict->alist (parse-dbus-reply reply))) + +(define (parse-contacts reply) + "Parse REPLY, which is assumed to be the output of the Jamid D-Bus +`getContacts' method, and return its content as an alist." + (match (parse-dbus-reply reply) + ('array + '()) + (('array dicts ...) + (map dict->alist dicts)))) + + +;;; +;;; Higher-level, D-Bus-related procedures. +;;; + +(define (validate-fingerprint fingerprint) + "Validate that fingerprint is 40 characters long." + (unless (account-fingerprint? fingerprint) + (error "Account fingerprint is not valid:" fingerprint))) + +(define (dbus-available-services) + "Return the list of available (acquired) D-Bus services." + (let ((reply (parse-dbus-reply + (send-dbus #:service "org.freedesktop.DBus" + #:path "/org/freedesktop/DBus" + #:interface "org.freedesktop.DBus" + #:method "ListNames")))) + ;; Remove entries such as ":1.7". + (remove (cut string-prefix? ":" <>) + (array->list reply)))) + +(define (dbus-service-available? service) + "Predicate to check for the D-Bus SERVICE availability." + (member service (dbus-available-services))) + +(define* (send-dbus/configuration-manager #:key method arguments timeout) + "Query the Jami D-Bus ConfigurationManager service." + (send-dbus #:service "cx.ring.Ring" + #:path "/cx/ring/Ring/ConfigurationManager" + #:interface "cx.ring.Ring.ConfigurationManager" + #:method method + #:arguments arguments + #:timeout timeout)) + +;;; The following methods are for internal use; they make use of the account +;;; ID, an implementation detail of Jami the user should not need to be +;;; concerned with. +(define (get-account-ids) + "Return the available Jami account identifiers (IDs). Account IDs are an +implementation detail used to identify the accounts in Jami." + (parse-account-ids + (send-dbus/configuration-manager #:method "getAccountList"))) + +(define (id->account-details id) + "Retrieve the account data associated with the given account ID." + (parse-account-details + (send-dbus/configuration-manager + #:method "getAccountDetails" + #:arguments (list (string-append "string:" id))))) + +(define (id->volatile-account-details id) + "Retrieve the account data associated with the given account ID." + (parse-account-details + (send-dbus/configuration-manager + #:method "getVolatileAccountDetails" + #:arguments (list (string-append "string:" id))))) + +(define (id->account id) + "Retrieve the complete account data associated with the given account ID." + (append (id->volatile-account-details id) + (id->account-details id))) + +(define %username-to-id-cache #f) + +(define (invalidate-username-to-id-cache!) + (set! %username-to-id-cache #f)) + +(define (username->id username) + "Return the first account ID corresponding to USERNAME." + (unless (assoc-ref %username-to-id-cache username) + (set! %username-to-id-cache + (append-map + (lambda (id) + (let* ((account (id->account id)) + (username (assoc-ref account "Account.username")) + (registered-name (assoc-ref account + "Account.registeredName"))) + `(,@(if username + (list (cons username id)) + '()) + ,@(if registered-name + (list (cons registered-name id)) + '())))) + (get-account-ids)))) + (or (assoc-ref %username-to-id-cache username) + (let ((message (format #f "Could not retrieve a local account ID\ + for ~:[username~;fingerprint~]" (account-fingerprint? username)))) + (error message username)))) + +(define (account->username account) + "Return USERNAME, the registered username associated with ACCOUNT, else its +public key fingerprint." + (or (assoc-ref account "Account.registeredName") + (assoc-ref account "Account.username"))) + +(define (id->username id) + "Return USERNAME, the registered username associated with ID, else its +public key fingerprint, else #f." + (account->username (id->account id))) + +(define (get-accounts) + "Return the list of all accounts, as a list of alists." + (map id->account (get-account-ids))) + +(define (get-usernames) + "Return the list of the usernames associated with the present accounts." + (map account->username (get-accounts))) + +(define (username->account username) + "Return the first account associated with USERNAME, else #f. +USERNAME can be either the account 40 characters public key fingerprint or a +registered username." + (find (lambda (account) + (member username + (list (assoc-ref account "Account.username") + (assoc-ref account "Account.registeredName")))) + (get-accounts))) + +(define (add-account archive) + "Import the Jami account ARCHIVE and return its account ID. The archive +should *not* be encrypted with a password. Return the username associated +with the account." + (invalidate-username-to-id-cache!) + (let ((reply (send-dbus/configuration-manager + #:method "addAccount" + #:arguments (list (string-append + "dict:string:string:Account.archivePath," + archive + ",Account.type,RING"))))) + ;; The account information takes some time to be populated. + (let ((id (deserialize-item (parse-dbus-reply reply)))) + (with-retries 20 1 + (let ((username (id->username id))) + (if (string-null? username) + #f + username)))))) + +(define (remove-account username) + "Delete the Jami account associated with USERNAME, the account 40 characters +fingerprint or a registered username." + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "removeAccount" + #:arguments (list (string-append "string:" id)))) + (invalidate-username-to-id-cache!)) + +(define* (username->contacts username) + "Return the contacts associated with the account of USERNAME as two values; +the first one being the regular contacts and the second one the banned +contacts. USERNAME can be either the account 40 characters public key +fingerprint or a registered username. The contacts returned are represented +using their 40 characters fingerprint." + (let* ((id (username->id username)) + (reply (send-dbus/configuration-manager + #:method "getContacts" + #:arguments (list (string-append "string:" id)))) + (all-contacts (parse-contacts reply)) + (banned? (lambda (contact) + (and=> (assoc-ref contact "banned") + (cut string=? "true" <>)))) + (banned (filter banned? all-contacts)) + (not-banned (filter (negate banned?) all-contacts)) + (fingerprint (cut assoc-ref <> "id"))) + (values (map fingerprint not-banned) + (map fingerprint banned)))) + +(define* (remove-contact contact username #:key ban?) + "Remove CONTACT, the 40 characters public key fingerprint of a contact, from +the account associated with USERNAME (either a fingerprint or a registered +username). When BAN? is true, also mark the contact as banned." + (validate-fingerprint contact) + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "removeContact" + #:arguments (list (string-append "string:" id) + (string-append "string:" contact) + (serialize-boolean ban?))))) + +(define (add-contact contact username) + "Add CONTACT, the 40 characters public key fingerprint of a contact, to the +account of USERNAME (either a fingerprint or a registered username)." + (validate-fingerprint contact) + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "addContact" + #:arguments (list (string-append "string:" id) + (string-append "string:" contact))))) + +(define* (set-account-details details username #:key timeout) + "Set DETAILS, an alist containing the key value pairs to set for the account +of USERNAME, a registered username or account fingerprint. The value of the +parameters not provided are unchanged. TIMEOUT is a value in milliseconds to +pass to the `send-dbus/configuration-manager' procedure." + (let* ((id (username->id username)) + (current-details (id->account-details id)) + (updated-details (map (match-lambda + ((key . value) + (or (and=> (assoc-ref details key) + (cut cons key <>)) + (cons key value)))) + current-details)) + ;; dbus-send does not permit sending null strings (it throws a + ;; "malformed dictionary" error). Luckily they seem to have the + ;; semantic of "default account value" in Jami; so simply drop them. + (updated-details* (remove (match-lambda + ((_ . value) + (string-null? value))) + updated-details))) + (send-dbus/configuration-manager + #:timeout timeout + #:method "setAccountDetails" + #:arguments + (list (string-append "string:" id) + (string-append "dict:string:string:" + (string-join (alist->list updated-details*) + ",")))))) + +(define (set-all-moderators enabled? username) + "Set the 'AllModerators' property to enabled? for the account of USERNAME, a +registered username or account fingerprint." + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "setAllModerators" + #:arguments + (list (string-append "string:" id) + (serialize-boolean enabled?))))) + +(define (username->all-moderators? username) + "Return the 'AllModerators' property for the account of USERNAME, a +registered username or account fingerprint." + (let* ((id (username->id username)) + (reply (send-dbus/configuration-manager + #:method "isAllModerators" + #:arguments + (list (string-append "string:" id))))) + (deserialize-item (parse-dbus-reply reply)))) + +(define (username->moderators username) + "Return the moderators for the account of USERNAME, a registered username or +account fingerprint." + (let* ((id (username->id username)) + (reply (send-dbus/configuration-manager + #:method "getDefaultModerators" + #:arguments + (list (string-append "string:" id))))) + (array->list (parse-dbus-reply reply)))) + +(define (set-moderator contact enabled? username) + "Set the moderator flag to ENABLED? for CONTACT, the 40 characters public +key fingerprint of a contact for the account of USERNAME, a registered +username or account fingerprint." + (validate-fingerprint contact) + (let* ((id (username->id username))) + (send-dbus/configuration-manager #:method "setDefaultModerator" + #:arguments + (list (string-append "string:" id) + (string-append "string:" contact) + (serialize-boolean enabled?))))) + +(define (disable-account username) + "Disable the account known by USERNAME, a registered username or account +fingerprint." + (set-account-details '(("Account.enable" . "false")) username + ;; Waiting for the reply on this command takes a very + ;; long time that trips the default D-Bus timeout value + ;; (25 s), for some reason. + #:timeout 60000)) + +(define (enable-account username) + "Enable the account known by USERNAME, a registered username or account +fingerprint." + (set-account-details '(("Account.enable" . "true")) username)) + + +;;; +;;; Presentation procedures. +;;; + +(define (.->_ text) + "Map each period character to underscore characters." + (string-map (match-lambda + (#\. #\_) + (c c)) + text)) + +(define (account-details->recutil account-details) + "Serialize the account-details alist into a recutil string. Period +characters in the keys are normalized to underscore to meet Recutils' format +requirements." + (define (pair->recutil-property pair) + (match pair + ((key . value) + (string-append (.->_ key) ": " value)))) + + (define sorted-account-details + ;; Have the account username, display name and alias appear first, for + ;; convenience. + (let ((first-items '("Account.username" + "Account.displayName" + "Account.alias"))) + (append (map (cut assoc <> account-details) first-items) + (fold alist-delete account-details first-items)))) + + (string-join (map pair->recutil-property sorted-account-details) "\n")) + +;; Local Variables: +;; eval: (put 'with-retries 'scheme-indent-function 2) +;; End: diff --git a/gnu/local.mk b/gnu/local.mk index 9e8f2d702d..66a2ba52bc 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -685,6 +685,7 @@ GNU_SYSTEM_MODULES = \ %D%/build/chromium-extension.scm \ %D%/build/cross-toolchain.scm \ %D%/build/image.scm \ + %D%/build/jami-service.scm \ %D%/build/file-systems.scm \ %D%/build/hurd-boot.scm \ %D%/build/install.scm \ @@ -722,6 +723,7 @@ GNU_SYSTEM_MODULES = \ %D%/tests/security-token.scm \ %D%/tests/singularity.scm \ %D%/tests/ssh.scm \ + %D%/tests/telephony.scm \ %D%/tests/version-control.scm \ %D%/tests/virtualization.scm \ %D%/tests/web.scm @@ -1881,7 +1883,8 @@ dist_patch_DATA = \ %D%/packages/patches/ytnef-CVE-2021-3403.patch \ %D%/packages/patches/ytnef-CVE-2021-3404.patch \ %D%/packages/patches/zstd-CVE-2021-24031_CVE-2021-24032.patch \ - %D%/packages/patches/zziplib-CVE-2018-16548.patch + %D%/packages/patches/zziplib-CVE-2018-16548.patch \ + %D%/tests/data/jami-dummy-account.dat MISC_DISTRO_FILES = \ %D%/packages/ld-wrapper.in diff --git a/gnu/services/telephony.scm b/gnu/services/telephony.scm index e1259cc2df..fd90840324 100644 --- a/gnu/services/telephony.scm +++ b/gnu/services/telephony.scm @@ -1,5 +1,6 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2017 nee +;;; Copyright © 2021 Maxim Cournoyer ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,16 +18,45 @@ ;;; along with GNU Guix. If not, see . (define-module (gnu services telephony) - #:use-module (gnu services) + #:use-module ((gnu build jami-service) #:select (account-fingerprint?)) + #:use-module ((gnu services) #:hide (delete)) + #:use-module (gnu services configuration) #:use-module (gnu services shepherd) #:use-module (gnu system shadow) #:use-module (gnu packages admin) + #:use-module (gnu packages certs) + #:use-module (gnu packages glib) + #:use-module (gnu packages jami) #:use-module (gnu packages telephony) #:use-module (guix records) + #:use-module (guix modules) + #:use-module (guix packages) #:use-module (guix gexp) #:use-module (srfi srfi-1) + #:use-module (srfi srfi-2) + #:use-module (srfi srfi-26) + #:use-module (ice-9 format) #:use-module (ice-9 match) - #:export (murmur-configuration + #:export (jami-account + jami-account-archive + jami-account-allowed-contacts + jami-account-moderators + jami-account-rendezvous-point? + jami-account-discovery? + jami-account-bootstrap-uri + jami-account-name-server-uri + + jami-configuration + jami-configuration-jamid + jami-configuration-dbus + jami-configuration-enable-logging? + jami-configuration-debug? + jami-configuration-auto-answer? + jami-configuration-accounts + + jami-service-type + + murmur-configuration make-murmur-configuration murmur-configuration? murmur-configuration-package @@ -74,6 +104,652 @@ (define-module (gnu services telephony) murmur-service-type)) + +;;; +;;; Jami daemon. +;;; + +;;; XXX: Passing a computed-file object as the account is used for tests. +(define (string-or-computed-file? val) + (or (string? val) + (computed-file? val))) + +(define (string-list? val) + (and (list? val) + (and-map string? val))) + +(define (account-fingerprint-list? val) + (and (list? val) + (and-map account-fingerprint? val))) + +(define-maybe string-list) + +(define-maybe/no-serialization account-fingerprint-list) + +(define-maybe boolean) + +(define-maybe string) + +;;; The following serializers are used to derive an account details alist from +;;; a record. +(define (serialize-string-list _ val) + (string-join val ";")) + +(define (serialize-boolean _ val) + (format #f "~:[false~;true~]" val)) + +(define (serialize-string _ val) + val) + +;;; Note: Serialization is used to produce an account details alist that can +;;; be passed to the SET-ACCOUNT-DETAILS procedure. Fields that do not map to +;;; a Jami account 'detail' should have their serialization disabled via the +;;; 'empty-serializer' procedure. +(define-configuration jami-account + (archive + (string-or-computed-file) + "The account archive (backup) file name of the account. This is used to +provision the account when the service starts. The account archive should +@emph{not} be encrypted. It is highly recommended to make it readable only to +the @samp{root} user (i.e., not in the store), to guard against leaking the +secret key material of the Jami account it contains." + empty-serializer) + (allowed-contacts + (maybe-account-fingerprint-list 'disabled) + "The list of allowed contacts for the account, entered as their 40 +characters long fingerprint. Messages or calls from accounts not in that list +will be rejected. When unspecified, the configuration of the account archive +is used as-is with respect to contacts and public inbound calls/messaging +allowance, which typically defaults to allow any contact to communicate with +the account." + empty-serializer) + (moderators + (maybe-account-fingerprint-list 'disabled) + "The list of contacts that should have moderation privileges (to ban, mute, +etc. other users) in rendezvous conferences, entered as their 40 characters +long fingerprint. When unspecified, the configuration of the account archive +is used as-is with respect to moderation, which typically defaults to allow +anyone to moderate." + empty-serializer) + ;; The serializable fields below are to be set with set-account-details. + (rendezvous-point? + (maybe-boolean 'disabled) + "Whether the account should operate in the rendezvous mode. In this mode, +all the incoming audio/video calls are mixed into a conference. When left +unspecified, the value from the account archive prevails.") + (peer-discovery? + (maybe-boolean 'disabled) + "Whether peer discovery should be enabled. Peer discovery is used to +discover other OpenDHT nodes on the local network, which can be useful to +maintain communication between devices on such network even when the +connection to the the Internet has been lost. When left unspecified, the +value from the account archive prevails.") + (bootstrap-hostnames + (maybe-string-list 'disabled) + "A list of hostnames or IPs pointing to OpenDHT nodes, that should be used +to initially join the OpenDHT network. When left unspecified, the value from +the account archive prevails.") + (name-server-uri + (maybe-string 'disabled) + "The URI of the name server to use, that can be used to retrieve the +account fingerprint for a registered username.")) + +(define (jami-account->alist jami-account-object) + "Serialize the JAMI-ACCOUNT object as an alist suitable to be passed to +SET-ACCOUNT-DETAILS." + (define (field-name->account-detail name) + (match name + ('rendezvous-point? "Account.rendezVous") + ('peer-discovery? "Account.peerDiscovery") + ('bootstrap-hostnames "Account.hostname") + ('name-server-uri "RingNS.uri") + (_ #f))) + + (filter-map (lambda (field) + (and-let* ((name (field-name->account-detail + (configuration-field-name field))) + (value ((configuration-field-serializer field) + name ((configuration-field-getter field) + jami-account-object))) + ;; The define-maybe default serializer produces an + ;; empty string for the 'disabled value. + (value* (if (string-null? value) + #f + value))) + (cons name value*))) + jami-account-fields)) + +(define (jami-account-list? val) + (and (list? val) + (and-map jami-account? val))) + +(define-maybe/no-serialization jami-account-list) + +(define-configuration/no-serialization jami-configuration + (jamid + (package libring) + "The Jami daemon package to use.") + (dbus + (package dbus) + "The D-Bus package to use to start the required D-Bus session.") + (nss-certs + (package nss-certs) + "The nss-certs package to use to provide TLS certificates.") + (enable-logging? + (boolean #t) + "Whether to enable logging to syslog.") + (debug? + (boolean #f) + "Whether to enable debug level messages.") + (auto-answer? + (boolean #f) + "Whether to force automatic answer to incoming calls.") + (accounts + (maybe-jami-account-list 'disabled) + "A list of Jami accounts to be (re-)provisioned every time the Jami daemon +service starts. When providing this field, the account directories under +@file{/var/lib/jami/} are recreated every time the service starts, ensuring a +consistent state.")) + +(define %jami-accounts + (list (user-group (name "jami") (system? #t)) + (user-account + (name "jami") + (group "jami") + (system? #t) + (comment "Jami daemon user") + (home-directory "/var/lib/jami")))) + +(define (jami-configuration->command-line-arguments config) + "Derive the command line arguments to used to launch the Jami daemon from +CONFIG, a object." + (match-record config + (jamid dbus enable-logging? debug? auto-answer?) + `(,(file-append jamid "/lib/ring/dring") + "--persistent" ;stay alive after client quits + ,@(if enable-logging? + '() ;logs go to syslog by default + (list "--console")) ;else stdout/stderr + ,@(if debug? + (list "--debug") + '()) + ,@(if auto-answer? + (list "--auto-answer") + '())))) + +(define (jami-dbus-session-activation config) + "Create a directory to hold the Jami D-Bus session socket." + (with-imported-modules (source-module-closure '((gnu build activation))) + #~(begin + (use-modules (gnu build activation)) + (let ((user (getpwnam "jami"))) + (mkdir-p/perms "/var/run/jami" user #o700))))) + +(define (jami-shepherd-services config) + "Return a running the Jami daemon." + (let* ((jamid (jami-configuration-jamid config)) + (nss-certs (jami-configuration-nss-certs config)) + (dbus (jami-configuration-dbus config)) + (dbus-daemon (file-append dbus "/bin/dbus-daemon")) + (dbus-send (file-append dbus "/bin/dbus-send")) + (accounts (jami-configuration-accounts config)) + (declarative-mode? (not (eq? 'disabled accounts)))) + + (with-imported-modules (source-module-closure + '((gnu build jami-service) + (gnu build shepherd) + (gnu system file-systems))) + + (define list-accounts-action + (shepherd-action + (name 'list-accounts) + (documentation "List the available Jami accounts. Return the account +details alists keyed by their account username.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + ;; Print the accounts summary or long listing, according to + ;; user-provided option. + (let* ((usernames (get-usernames)) + (accounts (map-in-order username->account usernames))) + (match accounts + (() ;empty list + (format #t "There is no Jami account available.~%")) + ((one two ...) + (format #t "The following Jami accounts are available:~%") + (for-each + (lambda (account) + (define fingerprint (assoc-ref account + "Account.username")) + (define human-friendly-name + (or (assoc-ref account + "Account.registeredName") + (assoc-ref account + "Account.displayName") + (assoc-ref account + "Account.alias"))) + (define disabled? + (and=> (assoc-ref account "Account.enable") + (cut string=? "false" <>))) + + (format #t " - ~a~@[ (~a)~] ~:[~;[disabled]~]~%" + fingerprint human-friendly-name disabled?)) + accounts) + (display "\n"))) + ;; Return the account-details-list alist. + (map cons usernames accounts))))))) + + (define list-account-details-action + (shepherd-action + (name 'list-account-details) + (documentation "Display the account details of the available Jami +accounts in the @code{recutils} format. Return the account details alists +keyed by their account username.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let* ((usernames (get-usernames)) + (accounts (map-in-order username->account usernames))) + (for-each (lambda (account) + (display (account-details->recutil account)) + (display "\n\n")) + accounts) + (map cons usernames accounts))))))) + + (define list-contacts-action + (shepherd-action + (name 'list-contacts) + (documentation "Display the contacts for each Jami account. Return +an alist containing the contacts keyed by the account usernames.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let* ((usernames (get-usernames)) + (contacts (map-in-order username->contacts usernames))) + (for-each (lambda (username contacts) + (format #t "Contacts for account ~a:~%" + username) + (format #t "~{ - ~a~%~}~%" contacts)) + usernames contacts) + (map cons usernames contacts))))))) + + (define list-moderators-action + (shepherd-action + (name 'list-moderators) + (documentation "Display the moderators for each Jami account. Return +an alist containing the moderators keyed by the account usernames.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let* ((usernames (get-usernames)) + (moderators (map-in-order username->moderators + usernames))) + (for-each + (lambda (username moderators) + (if (username->all-moderators? username) + (format #t "Anyone can moderate for account ~a~%" + username) + (begin + (format #t "Moderators for account ~a:~%" username) + (format #t "~{ - ~a~%~}~%" moderators)))) + usernames moderators) + (map cons usernames moderators))))))) + + (define add-moderator-action + (shepherd-action + (name 'add-moderator) + (documentation "Add a moderator for a given Jami account. The +MODERATOR contact must be given as its 40 characters fingerprint, while the +Jami account can be provided as its registered USERNAME or fingerprint. + +@example +herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f username +@end example + +Return the moderators for the account known by USERNAME.") + (procedure + #~(lambda (_ moderator username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (set-all-moderators #f username) + (add-contact moderator username) + (set-moderator moderator #t username) + (username->moderators username)))))) + + (define ban-contact-action + (shepherd-action + (name 'ban-contact) + (documentation "Ban a contact for a given or all Jami accounts, and +clear their moderator flag. The CONTACT must be given as its 40 characters +fingerprint, while the Jami account can be provided as its registered USERNAME +or fingerprint, or omitted. When the account is omitted, CONTACT is banned +from all accounts. + +@example +herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f [username] +@end example") + (procedure + #~(lambda* (_ contact #:optional username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let ((usernames (or (and=> username list) + (get-usernames)))) + (for-each (lambda (username) + (set-moderator contact #f username) + (remove-contact contact username #:ban? #t)) + usernames))))))) + + (define list-banned-contacts-action + (shepherd-action + (name 'list-banned-contacts) + (documentation "List the banned contacts for each accounts. Return +an alist of the banned contacts, keyed by the account usernames.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + + (define banned-contacts + (let ((usernames (get-usernames))) + (map cons usernames + (map-in-order (lambda (x) + (receive (_ banned) + (username->contacts x) + banned)) + usernames)))) + + (for-each (match-lambda + ((username . banned) + (unless (null? banned) + (format #t "Banned contacts for account ~a:~%" + username) + (format #t "~{ - ~a~%~}~%" banned)))) + banned-contacts) + banned-contacts))))) + + (define enable-account-action + (shepherd-action + (name 'enable-account) + (documentation "Enable an account. It takes USERNAME as an argument, +either a registered username or the fingerprint of the account.") + (procedure + #~(lambda (_ username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (enable-account username)))))) + + (define disable-account-action + (shepherd-action + (name 'disable-account) + (documentation "Disable an account. It takes USERNAME as an +argument, either a registered username or the fingerprint of the account.") + (procedure + #~(lambda (_ username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (disable-account username)))))) + + (list (shepherd-service + (documentation "Run a D-Bus session for the Jami daemon.") + (provision '(jami-dbus-session)) + (modules `((gnu build shepherd) + (gnu build jami-service) + (gnu system file-systems) + ,@%default-modules)) + ;; The requirement on dbus-system is to ensure other required + ;; activation for D-Bus, such as a /etc/machine-id file. + (requirement '(dbus-system syslogd)) + (start + #~(lambda args + (define pid + ((make-forkexec-constructor/container + (list #$dbus-daemon "--session" + "--address=unix:path=/var/run/jami/bus" + "--nofork" "--syslog-only" "--nopidfile") + #:mappings (list (file-system-mapping + (source "/dev/log") ;for syslog + (target source)) + (file-system-mapping + (source "/var/run/jami") + (target source) + (writable? #t))) + #:user "jami" + #:group "jami" + #:environment-variables + ;; This is so that the cx.ring.Ring service D-Bus + ;; definition is found by dbus-send. + (list (string-append "XDG_DATA_DIRS=" + #$jamid "/share"))))) + + ;; XXX: This manual synchronization probably wouldn't be + ;; needed if we were using a PID file, but providing it via a + ;; customized config file with would not override + ;; the one inherited from the base config of D-Bus. + (let ((sock (socket PF_UNIX SOCK_STREAM 0))) + (with-retries 20 1 (catch 'system-error + (lambda () + (connect sock AF_UNIX + "/var/run/jami/bus") + (close-port sock) + #t) + (lambda args + #f)))) + + pid)) + (stop #~(make-kill-destructor))) + + (shepherd-service + (documentation "Run the Jami daemon.") + (provision '(jami)) + (actions (list list-accounts-action + list-account-details-action + list-contacts-action + list-moderators-action + add-moderator-action + ban-contact-action + list-banned-contacts-action + enable-account-action + disable-account-action)) + (requirement '(jami-dbus-session)) + (modules `((ice-9 format) + (ice-9 ftw) + (ice-9 match) + (ice-9 receive) + (srfi srfi-1) + (srfi srfi-26) + (gnu build jami-service) + (gnu build shepherd) + (gnu system file-systems) + ,@%default-modules)) + (start + #~(lambda args + (define (delete-file-recursively/safe file) + ;; Ensure we're not deleting things outside of + ;; /var/lib/jami. This prevents a possible attack in case + ;; the daemon is compromised and an attacker gains write + ;; access to /var/lib/jami. + (let ((parent-directory (dirname file))) + (if (eq? 'symlink (stat:type (stat parent-directory))) + (error "abnormality detected; unexpected symlink found at" + parent-directory) + (delete-file-recursively file)))) + + (when #$declarative-mode? + ;; Clear the Jami configuration and accounts, to enforce the + ;; declared state. + (catch #t + (lambda () + (for-each (cut delete-file-recursively/safe <>) + '("/var/lib/jami/.cache/jami" + "/var/lib/jami/.config/jami" + "/var/lib/jami/.local/share/jami" + "/var/lib/jami/accounts"))) + (lambda args + #t)) + ;; Copy the Jami account archives from somewhere readable + ;; by root to a place only the jami user can read. + (let* ((accounts-dir "/var/lib/jami/accounts/") + (pwd (getpwnam "jami")) + (user (passwd:uid pwd)) + (group (passwd:gid pwd))) + (mkdir-p accounts-dir) + (chown accounts-dir user group) + (for-each (lambda (f) + (let ((dest (string-append accounts-dir + (basename f)))) + (copy-file f dest) + (chown dest user group))) + '#$(and declarative-mode? + (map jami-account-archive accounts))))) + + ;; Start the daemon. + (define daemon-pid + ((make-forkexec-constructor/container + '#$(jami-configuration->command-line-arguments config) + #:mappings + (list (file-system-mapping + (source "/dev/log") ;for syslog + (target source)) + (file-system-mapping + (source "/var/lib/jami") + (target source) + (writable? #t)) + (file-system-mapping + (source "/var/run/jami") + (target source) + (writable? #t)) + ;; Expose TLS certificates for GnuTLS. + (file-system-mapping + (source #$(file-append nss-certs "/etc/ssl/certs")) + (target "/etc/ssl/certs"))) + #:user "jami" + #:group "jami" + #:environment-variables + (list (string-append "DBUS_SESSION_BUS_ADDRESS=" + "unix:path=/var/run/jami/bus") + ;; Expose TLS certificates for OpenSSL. + "SSL_CERT_DIR=/etc/ssl/certs")))) + + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + + ;; Wait until the service name has been acquired by D-Bus. + (with-retries 20 1 + (dbus-service-available? "cx.ring.Ring")) + + (when #$declarative-mode? + ;; Provision the accounts via the D-Bus API of the daemon. + (let* ((jami-account-archives + (map (cut string-append + "/var/lib/jami/accounts/" <>) + (scandir "/var/lib/jami/accounts/" + (lambda (f) + (not (member f '("." ".."))))))) + (usernames (map-in-order (cut add-account <>) + jami-account-archives))) + + (define (archive-name->username archive) + (list-ref + usernames + (list-index (lambda (f) + (string-suffix? (basename archive) f)) + jami-account-archives))) + + (for-each + (lambda (archive allowed-contacts moderators + account-details) + (let ((username (archive-name->username + archive))) + (when (not (eq? 'disabled allowed-contacts)) + ;; Reject calls from unknown contacts. + (set-account-details + '(("DHT.PublicInCalls" . "false")) username) + ;; Remove all contacts. + (for-each (cut remove-contact <> username) + (username->contacts username)) + ;; Add allowed ones. + (for-each (cut add-contact <> username) + allowed-contacts)) + (when (not (eq? 'disabled moderators)) + ;; Disable the 'AllModerators' property. + (set-all-moderators #f username) + ;; Remove all moderators. + (for-each (cut set-moderator <> #f username) + (username->moderators username)) + ;; Add declared moderators. + (for-each (cut set-moderator <> #t username) + moderators)) + ;; Set the various account parameters. + (set-account-details account-details username))) + '#$(and declarative-mode? + (map-in-order (cut jami-account-archive <>) + accounts)) + '#$(and declarative-mode? + (map-in-order + (cut jami-account-allowed-contacts <>) + accounts)) + '#$(and declarative-mode? + (map-in-order (cut jami-account-moderators <>) + accounts)) + '#$(and declarative-mode? + (map-in-order jami-account->alist accounts)))))) + + ;; Finally, return the PID of the daemon process. + daemon-pid)) + (stop + #~(lambda (pid . args) + (kill pid SIGKILL) + ;; Wait for the process to exit; this prevents overlapping + ;; processes when issuing 'herd restart'. + (waitpid pid) + #f))))))) + +(define jami-service-type + (service-type + (name 'jami) + (default-value (jami-configuration)) + (extensions + (list (service-extension shepherd-root-service-type + jami-shepherd-services) + (service-extension account-service-type + (const %jami-accounts)) + (service-extension activation-service-type + jami-dbus-session-activation))) + (description "Run the Jami daemon (@command{dring}). This service is +geared toward the use case of hosting Jami rendezvous points over a headless +server. If you use Jami on your local machine, you may prefer to setup a user +Shepherd service for it instead; this way, the daemon will be shared via your +normal user D-Bus session bus."))) + + +;;; +;;; Murmur. +;;; + ;; https://github.com/mumble-voip/mumble/blob/master/scripts/murmur.ini (define-record-type* murmur-configuration @@ -305,3 +981,7 @@ (define murmur-service-type (service-extension account-service-type murmur-accounts))) (default-value (murmur-configuration)))) + +;; Local Variables: +;; eval: (put 'with-retries 'scheme-indent-function 2) +;; End: diff --git a/gnu/tests/data/jami-dummy-account.dat b/gnu/tests/data/jami-dummy-account.dat new file mode 100644 index 0000000000..0e908396ca --- /dev/null +++ b/gnu/tests/data/jami-dummy-account.dat @@ -0,0 +1,392 @@ +;;; -*- mode: scheme; -*- +;;; JSON extracted from an actual Jami account and processed with +;;; Emacs/guile-json. +(define %jami-account-content-sexp + '(("RINGCAKEY" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3F\ +oa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzBxWUozSkYvTzhQRGEKRnUwRnpRcHBCaD\ +gybGJMdURrNTlVU0I0MUJSaS9kdDZGV1BRN29YOVpsY25vNGZzM2dmUHQ0dU1hRVBkVFBGKwowbGN2Q\ +jc2cytQTEFlcjlOZGpVQzQ2ZXp0UnNiNE9aQXc4ZUk1M3EwSU04QWJFd0o0ZjllLzBmQUFueHgrK3Qw\ +CmZDeGV1YTBUaVBqVHBpZVJMNmpwQkd5UGI2Qk5pU2ViTkZCNzJOMTBnbzI4REVQYlhkNE9CNkN1blZ\ +5RGZKSU0KOC9PRy8rMndNamI4WkRwT3JrYy93U2ZHbnQyZXA3U0xKSkgwOFIzV1FKWklsSndrcGdLTH\ +FRakVwWFNpclN4dAozSkdtdXdBdE9LaXFFTXh1R043elV3ZlNINEtHdkRaUFNkZklZVXJ3eEp4aDJZZ\ +3lobG5RRC9SSnhRN3d4YlJBCjFhMUZVV0FzbDhLODk5cEtESk1GL09VOWZMRUx6QlViblpaRDRmSlg1\ +NmcyTlluUnJobS94NG9FbFk0MFNYMUUKcHYzb01hNnZrVGN5RjJnUFhKL2FkUVJoS0dFaGRjaHBpeDl\ +5UVphaDFCUFBGYW5jNzBMcjhOaDZJeHFNQ1hiMQozMG9vWHpWZmZNMVFOd28rL3hzRnBlRkRqUTAxQ0\ +9pdWZocitKREcyc0txb0o0V0JwYVhubWI1YXVrVWUvV1RKCjAxVmRyaEkvSVExd3V4QzNMMnpac3dVU\ +1NTaDk0aXg1M0hpU3pWbkI5UkxmaVhZUUVCcFEyNHVoRTdiYlo0bm0KZTczZC9zenpPTXMzYUt3OWtW\ +a2VLMTVtYWhSVWZjdEdhSVQxTkhGWUNYYXByaWExakdNdVpmSk1pSUtZUzNidQpMbUhZckF6dEptNDZ\ +0aHpjdnN3NHlhMnFoa2xUUlFJREFRQUJBb0lDQVFDaHZaUm85KzZ5aFhFTHZ6U3FXZHcxCkZGOERibG\ +hIMmhVWkNuV0kxMDM5SmdyRkxMczFSU1krSzg1aFZYMk9hV1VTNk44TmNCYzUyL1hrdFltS09HUFQKM\ +WZqMnE2M3pPcDNSSFdGNWVPMXhNeExRN3JZSDhqMGZZTFFTUytKemdwb3ZRVnJLSXkrb21JSSt3aUN6\ +R1laRApGQUM0ODJzL0J5MHdtRjVjdC9JTEdIeVY3ZXNVUlo1Vi9iL0ltQzUwQ1lDUWpQR2xBb3JkeUx\ +1MHp2NjZZUXc2CkQycTg0VHAyVUg3SExEVmhFNytUbDg4Q04xWll0VGtpSkthbkNpMFVmbStPKzJFM0\ +5HM01hajk1aDl2NktqYkoKUlkxeTNDRTVmQmkyUFNLbVVzRjN3SzdhbzJDRks5MTgybmlxL2FaNm5WO\ +Xc3NmVrRjhEOWUvS1pqUE5ZT0xkaApFczBSL2laV3RpbUx2RHdXQWNWNFNnSFFjNXJvNU9yOEFUS1ZK\ +VmlzZGFuWXkvdUhmVXZWN3U5cDVLK2E4SHU2CllabW13ZTh4bnF5M3V2M0VabE9LY20zTnZvWllVMnJ\ +HUUFQQW1sWWQ4WlRsZGxPa1JCSGxxYzllMmJuSnNTQW8KNUhhS0N3aDJsWmZpalVGNXFrMXNQcm1kN3\ +BlMld2VVV3QmVuSjJnS1ZoTE5VVGtHWmtTWGhzNlV3WWRRMjVtRQppQzl6WjhXNkQ2OXBvb0lsTTVXT\ +01ySEs0Rng1ck9vT05kUHQ4NEk1bTI3cnpnbXM4QnJXVUlGLytZZjJ0bGdmClBIR0V4c3ZCK3JRQk52\ +WHU3dXoxcVdFTlJTL2YwR2E3dVF4ZW5sZ2dubHc5M1pNOW1GWXpXb1RpdWFmdnphTnAKWEsrTEVrV2F\ +RYUs1Q0VaNEhmUlhBUUtDQVFFQTUyK240OUxQODlyQkR2bFdsTkxNanJqTDdSb0xyQ3FVZGpQWQpyT1\ +hZS3ZkTkxyS2NTc1hNdkY4YW5HQng1UG5oVDZGY05ic2dzQ3BUUXowMThZYmcrbUUxQno5ZTdFNTJGZ\ +i9NCk9BbWZqSllXUnZueUtiNnB3SGlvOHlXUXlVVk1zZU1CcmpvWk1kNkpPZEZ0K2JITHBWOS9iSkdR\ +a3NTRE04WTkKbWxGQUlUL0gyNTh1K1ZKTWsrT0prU28zZmJQSk5Ja1Q2WVBKVmNaSnZTRGI5QU83WDF\ +lendCOXVRL0FEblZ6YwpSQkJOUVZaTStZS2ZNWFJBdmFuWnlmWFFwaUxCQW8rRVRPSHJCR1dDRUhtSF\ +RCaEZIMTkyamtxNlcrTStvS0R1Cm1xMitMc2hZWTVFc2NpL2hPOVZjK2FCM0hhaGliME03M092MHFNc\ +WZoTncyU1BncHdRS0NBUUVBeDlaR1gxQnQKL3MwdGtNcGV1QWhlWjFqTklnZmFEY0RRTWlmU0k4QjRx\ +WUhiL1hOREQ5NjFQME9zMDdCN2wzNE1iZ3U2QlNwcwpXdSt0Y1hjSFlqQVJUc0Qzd0pSaVRIb0RSQzR\ +YYkxEa2pHRUVCVzRKbFVqZTA1QWZrU0QrdkZSMkJwZStxQlBLCm5yb3Mwd1BWL3RXa3MzY2VFOUlBTV\ +pWWDhQeFA3RVNXbitVZDJEWkZhcVFLb0JybHZXRXhxelpYUEJSVjhoS3QKcFBqWnFkZXFQLzhUZTBtS\ +zh5MEVreXVXOWhFdGZ2Sm5HWXhMNStrSG9xd2hQVk1tODZ5YlZNVHRQYWJTdCtPUwp0WHhJTE9RMWRN\ +QkFabzRxSnNkUUZJcTJnSHA5WFYwa2ZNUms1ajdJT1Q4c2Z6TlpKVkRNK1k5VHVlSGJXSnduCnZsWld\ +VZ2NVZTlBaWhRS0NBUUVBbFJaK2h1ckUvNGdLR2dWUld5bTRrTEJHM2dTTFJHdGhuQXVtSnlzaFoveE\ +wKZ2l1Wk55bll5L2hRQWpDMjdoUnlxb04rRFRid3hjdGVPOUJ3c1poNzBZOVJROHYwOERGVExMVE43O\ +E56UG5OcApBbXY5TGhzZTYxaFBMZU1qTkNVcVZPV3hyWFRMeWk1YkpCM2Z4SnhlWGJmNU5BMUpudUpz\ +eXF1SC82TWJ0cytKCmhkY3p3WFRjMCtBZVBKOS9nOENQZXdKYkMzRFVBQ2R1VlNHWHo4ZWZxcm1xbDd\ +jbnB5ZzBpK2pJRkNpVU8rVEcKVFcxeDg3KzUvUFF2MGtSQ0Z1UUloZ2ZCNkcwWW9vcHBrUWRZdXhKZl\ +pPaHdUUldpbTVMMlF5K294WWZySGVQOQozSlltbGFCMmJiN3kxL1FoQjcvek9VMk1nTEtYdHl4Z09ve\ +EpoQlFwZ1FLQ0FRQkIwSUE4dy9CMkNuMEhRcDhQClhUSTZOelRZRUYzd1NhQkg1SFdBOE5MTWdNaERJ\ +TUxsWnlPcVFrK1pLSGFMM2llWjFxTGRNS3VmQjNESC9idWcKeXRQb2JBVXNsN0lJSGVjVmZWaVpvMml\ +pRXhHUCtEMlB2UUFtRFVGWU90V3FrT2FPSlV2VmJ5ODhOM1NyeW9lZgo5aHpZUGxMWmxFQWNGR055S3\ +FibjJXOENHaU5LSWhXYW1Zd21UclY3T1pkeUcrTi9GZk40Vms1NkZyc1pCTDQ5CmRYU2xGZ045TTBaZ\ +WNleTEvZEpPRE9lSHNuME5VK0gvNFZEUk1hR1NmelpwSkxJOXE4T2FiSWpVM0ttb24wQTcKdzFWeWNU\ +L1FwYlBxRUFVckt5dytvMzV3MlAyaUZ1czZiMlBvUUxFTGFTRVl6K3R6UEw5UTM1ejNRdGdMQytuagp\ +IUmxCQW9JQkFES0Q4NGhrYkphczlIQ00zanNNSU9kb213ZEMvcktxVUxKNHFWZU5QR0xDY2VpNEdocn\ +dlQnNICnNoN0hibFlZSDN2U2Y3RW1iZEVCb2xlWVJsaUx5ZzJDQU1ZOGpNK0lpUEwydk1yM2Y3SzZPT\ +mU0UkVPUFJSZkcKWlJDcTh4a0ZPQlg1SUJUbkVCV3QzdVdyb1NGY2x4RTdUa2I4VkUyVzExTG9ZNlkw\ +TUNPaHdxN21xYXRUVnNrawpTRDNySmkrTFR6a2Y4OEx1bjZZNjdiaFNOTWpKZkFaUXNQc0FTRkJBUTJ\ +rQnE5alRLZGVuaU4yYTJIbm0xNCtrCnJDeU9ZVE14Q2hQbWNpS25pVy9MWnFUL0U1dlNRUGdBVzc0dT\ +VLazJoSjRBajNjRW9NVEwxSytZbStWYWh2U0cKTi8xOFdYQ1JRQkg1d0p2eXJYczBtT29GQlRnTWg4d\ +z0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=") + ("ringAccountKey" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTk\ +Jna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRRDNCdDRnOUVUdk9EVnYKM3hWV0ZlS\ +1Nnbk5uVEF3S2dYa3IrQ1FhdU8vTGZWM01RenNSNHliL3hoaWhhb1Z2c2FtZ3ZRU1haL0M1R3I5QQpE\ +YlAxbHNHclRCK1pXMC9uMXVEb3hmVWdZRWY3SGtVanJtdVFjUGlFWGlUVkNiY002U0NzdVNrMnRxdE1\ +BNjBNClRacVo2LzAvbkEvblR5TnFNaUJNSmFRN0NUb2xOTTh2Z2tvd0tBRW14OGpJUG9YZEttMnd6MD\ +Z6SnhwU1d4WjUKc0FBTkdSSHU0b2ZXNWJiMERvamtnRzFBYUJ6Nm9uSmdKK1JFSWV1UkpNQWFHYmtzW\ +TQ4Z1Z1b21BVTU0UFNvUApFb3psVGdHd1k0cnhJTGE0V1Y0RVZMVExrR240ZTYrRUYrSjIvRURBUFdN\ +bXREdFpUeURCc2t5bStLaDRUT2xPCjdnN3JlUUhLdDd1R21YU0RnbzVKZ2hOOHNVRFUxL1Z2YmFFcUZ\ +tTDJrZWNGOVlVZmNsUWRGY1ZncmIrMkh5R3AKRVc0b3RkZjlYYzhOMWxrbGk1dFBqRGZuQ1U3OHB6QT\ +BxQmV1SWhZTnF1VjhGSm1NemhXeVFDbE1MUEFYbXFVdwpWYlY0MWduM0NkektuUlVhZXFONXlzOG5KQ\ +mRJNDBleWlYRUlvU0VKcFpyT1Z5ck1icnNCQzltaVFpZUFhSnlBClFvcjFaRGlwMkpZNUFza2phUGQ0\ +NGk0MGNkeFpob1RhNnpzako5UjZScllFYjhWTmZQemFLaElwSXJSd2NpbCsKWjFrbUUwSE1kY21ITXR\ +hbWZhK0l6WUR4dDV6OGVOZys1RGpVVG1MRkdyQUVWRW5hNDcxdllxYnk5UCs1T1cvNQpSdkxtUER2Tj\ +dlUURpTlZENlBYRFk5OTY4bTZaaHdJREFRQUJBb0lDQURjOEkrTCtlNE41OEFqcHV0MmEyeVNqCllxY\ +VFUSWowMW1GTWhOWXMwQUdTTUswQncyMkdleXZwNFl3R1EzdnNIOSsvSkEydXdoYkJzazNpUW9FQmlx\ +Q0EKenZmOWdPcDRFNlk0elV6RitwSmQvRnUwSG4wWHBab0Rhdnp2eFN4djNFeUN3b0puYWZuL1FHeGw\ +xZEhoQUtsKwpmZGZjekRCc3NPZ1Y2cGtBd1MyY2wwOHFOT2g3cVhaQWFkYk1sQ1lWM0owU1hhaVZiNz\ +lHZXNvTzNwUVBMUUZiClNjQjFjT2sxYnNxWkpOU244d0xmMis5QVBEdzMwWEtNNHg5eTdRTE42Q3oxQ\ +WpvcFJLQ0NIS3R1SEc4UmVETTIKcnRTbjJmTnltQ0VqeDZGVTB6MHFldDV3Y01UbU5weEZuYXdEMU5s\ +dFpnZXBsSllwTjVKZXNEUmo2cFlnWXBPKwo5UDU2cEdtZVNTVzVxcHRoWFFLQmFsdy8wWXp2YUlYdHp\ +hTnZyNUJzRFNrWkU3cldzODIvQmFzUE1RckFmOGpLClZFMU9pSzcrVllUVTRFVWVoZ0FZOXdzNjFqYk\ +lSSEpQbW9VQXpkWHcyL0d0Vk9JUTFwUFJnOXNYN0JaMmUyV1YKdmd5aThPUEJxRWtwblBiMkU5Z0d1U\ +25rY01OeWFVbUl0c3pFandadW14dnZrUW5HanlTb3pjY2R3dnNQNnBJagpoN0g5VUNQTHdOM0N5N1lp\ +UmliSlZBWlNjZkF6QzhubXNLODQrTzJUZHBzTXk0SEZDMmM4dlZiclpteTVkWC9qCk1ESnBzS25JWlZ\ +JMmpXSzRpRS82aUdIWVdoY3JvWnYvVEJ0YW1SQUxTWDVOYkhhWTI0bXVRSG5yMldtaytld3EKbHRGbC\ +90bXgyVkpWUitMZ0JCUGhBb0lCQVFENEI0MjQyVTgvbkJ4d2RzelhCdWxBOVFTa0I5Zktud0RlRkV3S\ +wp3Nks0eU14YVdRU04ycjRxRDhLcW93OVZVMzRYdkRWbFh0RUlDaVh3Q0hZdW5IL3g3cXNSdEVzbHJM\ +aWg4UHRPClpDSU8zUml4RmlIQXFlQUh2YXF0NVhXdndaREx6WnV2THRJOTdINkZ1QjYxck5qMnhxdlR\ +IN05pUmp2S0R0WXgKR1VtNURoQ094cm9tR0NkWHRnWHJGaS9WRU1TSmpQMkM3OTNrN1pTNmNZL0Nkc1\ +RqWEsrZ1UzeWM3OC9kN1pYbwpKMGg2WFlSdmhlQW9Bd2dkVTl4MWtYL2ZmY2tZK1hUcFRwV2xXWmtlU\ +ytsejBpQkRnUlJzMm45OFZDeTZDRmRZCldsZXZaZy9SWXZ6dzlKdWFVcXArOHpHbHNXR2xuOEhtZW5X\ +Q1luSHJnNFBxQnRkL0FvSUJBUUQrOXhDL1N5ZGIKVWZxMUJHYy95YktZc3RLb3A1azB0d3M0SlUwTzN\ +aT1U3MlZ5YTdLT2lTemdPSzVnL2QzckhMMXR2dHViTDBWNAo1dEF4a1AzSkVYbmxZZU5IVkpROTc2RH\ +NGS3Ztc0FGL2JJdVBsdFBRT1dyM3g1eW1RU3lCOTBUczV0dFdWMTVQCktYYVNnMTZidDhwNS9MeERkZ\ +ng2c1YzN1o5RDFRd21EQllreVFIcWQ4clljTm9ad1M5ZnI3UTZhN1ZNSDVtT3IKbEF5dzBCYVdZQk9k\ +bjFGd0pVV3NlRlpmTy9vNUVqZk9Hd0xMR1hiOEVmQ1hqdlRYcUNHLzNrT1JvN1NkOWY1eQowVjIxMmt\ +YVVNINHNDbFB0SmwzeHpaMWJxQ2RMVDNITWNLUTk5UGFFVFppQnNXQ1lOcXg1c0Q0RGhoMzdZQ2hKCm\ +hlN3VUM1E0MElINUFvSUJBREN1VXR1b0UweloyQjhld2grbUpKdnlPMEh5cENFSnlrTE1Xd3gxejNkV\ +E9nQzEKbmhZMWk4TjNxbTZSYUk0SHdDVHFkTlI3b3ExZ1NJZnZNVHIrem9IdXBUYnBXeUorM3hJeDJU\ +Rk9wL3lnMnByUApURHFqWE94SUJycnc0WU5vaTRIa3poeTVKTnl3a1RpdnBaOWsySVMvQTdTQmNWVGx\ +raENianVDK0pPRWthSTJOClpiWGFZY1p1WElVQ3FzcTM2c3RRbCtWZUxRQWt2VjlHc0wrclRnT09Dbz\ +UrTkdRZEVZQnVoRkMzZlJzL1JhSVoKOWFBRTBFL3BTTWp1a05tTnQ2Mm1NSk1tTUdydXhnWFRRblBRR\ +npNSW43aXB2Z0hxQjRsUDM4emdsbnMvbmZVcgo1NWRuZXk3ejhMRFFETHVIc0RHd3hINzNKQjgrTVR2\ +WGFVbkNwQU1DZ2dFQVNBSGxBL0dvdXR6TFRvWmcxcDRUClI1YnhjZHBycFh5d3VYbW5hclJmY3VldG9\ +nUVNtTGpiS0xRNVk0RXZSTENJTzA5MDNENGNnOG5FTU10L01XTXoKSnZwZll3emJGU2J4THR1anRQSX\ +VhaHR3eXV2UkJIVEM1aG5FL3h0WEE1bWZLTDBHWXpzbmtubm1WL2lzSnBSZwpwZFVnSW5sWEJodkRyR\ +FlreUsvWEp0N1FZWlhlUzI5NXlUd0krZndoamlzVVBlTWEyUmRUUE9rQ01JbUVaNUhZCjJHSmZjS25H\ +SkxDVHpDKzNPcGtQazdFRE4vTUlMS2F3YVUxaGp1cVlKWVVUVmpXQzFEM2VUL1ViWHptM0VQNHMKVEN\ +uYWpCYVMzN0N2YVd4ek5JektXZS9TSXdGbEFmYWNSTHlneUR4Z3Q3bHp1akVObEtvU2xya3h3ckpEND\ +Z2WAptUUtDQVFBcTVQWWxSQjgvNnFiWWt1OTA0NUZRdXk2QWtlYXBaMW0raS9SQzZtbFRvUXB6NDlPU\ +Gs4ZGx5YjVtCndvSVhpaEo2V05jN1RsWlRYMnpQTTRBS0M5VFNBUWJrWDg5bjEyU2VDSUlHbXVINnk0\ +TjZiY2lxZjVVcSsvc0IKcHJKeFRNYlRSUFpqS0VVd1N0SFg1MUQ1bi9sQnZERGY3Y2VEZytsYlE0RjR\ +KMTlPd09oZ1lGcjFheGQvNXd2VgpURjNoVlQwbFZGN2RyRC9iMHZOcmxnbUNjbEk4UDg1a2dkRUhZbG\ +ZtTFoxeXJIMkNXVy9SS0lsWk9ZdFVuNFNpCkp5a2VlNDROWElXU3ovalRBdFRta3VQTzRvUjF5d3dRc\ +jdhUTF5a3hRVm9rVm5vY2xqU0tyQlk4R294a0I0eDIKUDNrb3F1UnkvcUd3QzBnN1o4ZjBTQjNQZVZt\ +eQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==") + ("ringAccountCert" . "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ\ +0F3SUJBZ0lJRm1tNmZuaWRndEl3RFFZSktvWklodmNOQVFFTUJRQXdUREVRTUE0R0ExVUUKQXhNSFNt\ +RnRhU0JEUVRFNE1EWUdDZ21TSm9tVDhpeGtBUUVUS0RjNVpqSTJaVFZpWTJNeU9EWXlPREppT0dFMwp\ +PRFF6TUdOak1EWXpNakV4T1RFNFkyWm1PVGd3SGhjTk1qRXdOREUyTVRjek1qRXdXaGNOTXpFd05ERT\ +BNVGN6Ck1qRXdXakJKTVEwd0N3WURWUVFERXdSS1lXMXBNVGd3TmdZS0NaSW1pWlB5TEdRQkFSTW9aa\ +k16TkRWbU1qYzMKTldSa1ptVXdOMkUwWWpCa09UVmtZV1ZoTVRFeFpERTFabUpqTVRFNU9UQ0NBaUl3\ +RFFZSktvWklodmNOQVFFQgpCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFQY0czaUQwUk84NE5XL2ZGVllWNHB\ +LQ2MyZE1EQXFCZVN2NEpCcTQ3OHQ5ClhjeERPeEhqSnYvR0dLRnFoVyt4cWFDOUJKZG44TGthdjBBTn\ +MvV1d3YXRNSDVsYlQrZlc0T2pGOVNCZ1Ivc2UKUlNPdWE1QncrSVJlSk5VSnR3enBJS3k1S1RhMnEwd\ +0RyUXhObXBuci9UK2NEK2RQSTJveUlFd2xwRHNKT2lVMAp6eStDU2pBb0FTYkh5TWcraGQwcWJiRFBU\ +ck1uR2xKYkZubXdBQTBaRWU3aWg5Ymx0dlFPaU9TQWJVQm9IUHFpCmNtQW41RVFoNjVFa3dCb1p1U3h\ +qanlCVzZpWUJUbmc5S2c4U2pPVk9BYkJqaXZFZ3RyaFpYZ1JVdE11UWFmaDcKcjRRWDRuYjhRTUE5WX\ +lhME8xbFBJTUd5VEtiNHFIaE02VTd1RHV0NUFjcTN1NGFaZElPQ2prbUNFM3l4UU5UWAo5Vzl0b1NvV\ +1l2YVI1d1gxaFI5eVZCMFZ4V0N0djdZZklha1JiaWkxMS8xZHp3M1dXU1dMbTArTU4rY0pUdnluCk1E\ +U29GNjRpRmcycTVYd1VtWXpPRmJKQUtVd3M4QmVhcFRCVnRYaldDZmNKM01xZEZScDZvM25Lenlja0Y\ +wamoKUjdLSmNRaWhJUW1sbXM1WEtzeHV1d0VMMmFKQ0o0Qm9uSUJDaXZWa09LbllsamtDeVNObzkzam\ +lMalJ4M0ZtRwpoTnJyT3lNbjFIcEd0Z1J2eFUxOC9Ob3FFaWtpdEhCeUtYNW5XU1lUUWN4MXlZY3kxc\ +Vo5cjRqTmdQRzNuUHg0CjJEN2tPTlJPWXNVYXNBUlVTZHJqdlc5aXB2TDAvN2s1Yi9sRzh1WThPODN0\ +NUFPSTFVUG85Y05qMzNyeWJwbUgKQWdNQkFBR2pRekJCTUIwR0ExVWREZ1FXQkJUek5GOG5kZDMrQjZ\ +TdzJWMnVvUkhSWDd3Um1UQVBCZ05WSFJNQgpBZjhFQlRBREFRSC9NQThHQTFVZER3RUIvd1FGQXdNSE\ +JnQXdEUVlKS29aSWh2Y05BUUVNQlFBRGdnSUJBRDMrCjlscFluMjZyeG5pekY2UkNvZFFFVmgvOVF2R\ +Wp1V3dHUWZqa3gxb3VlVjdDMzUyWnpIT2hWU3VGcG43TUxkVFcKamI2dWhMRkpoMWtlTDlYZ0pHalMy\ +V0UwdDlJUGp2UWx5UHIwRWJoWGRJNDJMYUR0NDk0dWQ2MEE0bWg0bW1zbwovcjY2NERKOWMwUjZBOUd\ +QZUJjWi9zekhMQjJ4VmM5M3hYNjVjcmNoVTFLMDBVK2ZZUWtHS0xidFFXeUZzdnlKCmRHdFRxamVlYV\ +VNeGt5dFFWNWFxVHJ1SG4vV3U2RWRZejYrZ0ViQXJHUURRK1J3QmMxMDNGcWRIU0xReWdEWHoKd0pNV\ +GhDREdBVmR6V2NCemJYL1JIZms5bTZzK01HblNJUFBrNG9FOUdFbGdQc2JUZ2FrTzc3aElUdzRxZXJi\ +TAp2M0tiaTVOeVZZVFNVa3hDb0VRSkIrWlZJT3BVNFhRdVAvNkZkWldBRk1LU0Jkd1JLcnRmT1hZT1E\ +xcVBvYU1uCjZPR1VGMU0rYWZ0dTNCMkNhZ2ZMaE5hbVBoSjdxWTNSMzJhK2VRVllvYWlESXZKMElIUH\ +FnQ0NuWXlIaVBHTjQKS3VvaE9IZFpXTkxOUXdIQTg3SDV5NEwrSGZISmdFZFppdWYrbTNMa3JJcnN5M\ +WFoUjgyaEpZVkhreHVyZVRDcQpIR2NJaUIvTHpkSG91WFVrWGNrNjRvWXVRQnM3ZE9KVEx6bGlibU8x\ +elRzVWRoc0d4dE9zc2lYWkFjUURmZHBnCjNHdFJ3UkRwd0ZtL2k5TC9UWjMzMjBwY0VZd21aN1dBSzJ\ +Ra3Z6eWZlelRGeXdLWmh3c0RQRkwycllzLzhXdWsKRk1sek5BVmRMTytTS1RxeGlkM0l1elFaQ0FwU0\ +5ybTJMY2ZVN2grWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBV\ +EUtLS0tLQpNSUlGWFRDQ0EwV2dBd0lCQWdJSXV0T3MxUXBsSVRvd0RRWUpLb1pJaHZjTkFRRU1CUUF3\ +VERFUU1BNEdBMVVFCkF4TUhTbUZ0YVNCRFFURTRNRFlHQ2dtU0pvbVQ4aXhrQVFFVEtEYzVaakkyWlR\ +WaVkyTXlPRFl5T0RKaU9HRTMKT0RRek1HTmpNRFl6TWpFeE9URTRZMlptT1Rnd0hoY05NakV3TkRFMk\ +1UY3pNakExV2hjTk16RXdOREUwTVRjegpNakExV2pCTU1SQXdEZ1lEVlFRREV3ZEtZVzFwSUVOQk1UZ\ +3dOZ1lLQ1pJbWlaUHlMR1FCQVJNb056bG1NalpsCk5XSmpZekk0TmpJNE1tSTRZVGM0TkRNd1kyTXdO\ +ak15TVRFNU1UaGpabVk1T0RDQ0FpSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0l\ +CQUxTcGduY2tYODd3OE5vVzdRWE5DbWtHSHphVnN1NE9UbjFSSUhqVQpGR0w5MjNvVlk5RHVoZjFtVn\ +llamgremVCOCszaTR4b1E5MU04WDdTVnk4SHZxejQ4c0I2djAxMk5RTGpwN08xCkd4dmc1a0REeDRqb\ +mVyUWd6d0JzVEFuaC8xNy9SOEFDZkhINzYzUjhMRjY1clJPSStOT21KNUV2cU9rRWJJOXYKb0UySko1\ +czBVSHZZM1hTQ2pid01ROXRkM2c0SG9LNmRYSU44a2d6ejg0Yi83YkF5TnZ4a09rNnVSei9CSjhhZQo\ +zWjZudElza2tmVHhIZFpBbGtpVW5DU21Bb3VwQ01TbGRLS3RMRzNja2FhN0FDMDRxS29Rekc0WTN2Tl\ +RCOUlmCmdvYThOazlKMThoaFN2REVuR0haaURLR1dkQVA5RW5GRHZERnRFRFZyVVZSWUN5WHdyejMya\ +29Na3dYODVUMTgKc1F2TUZSdWRsa1BoOGxmbnFEWTFpZEd1R2IvSGlnU1ZqalJKZlVTbS9lZ3hycStS\ +TnpJWGFBOWNuOXAxQkdFbwpZU0YxeUdtTEgzSkJscUhVRTg4VnFkenZRdXZ3Mkhvakdvd0pkdlhmU2l\ +oZk5WOTh6VkEzQ2o3L0d3V2w0VU9OCkRUVUk2SzUrR3Y0a01iYXdxcWduaFlHbHBlZVp2bHE2UlI3OV\ +pNblRWVjJ1RWo4aERYQzdFTGN2Yk5tekJSSkoKS0gzaUxIbmNlSkxOV2NIMUV0K0pkaEFRR2xEYmk2R\ +VR0dHRuaWVaN3ZkMyt6UE00eXpkb3JEMlJXUjRyWG1acQpGRlI5eTBab2hQVTBjVmdKZHFtdUpyV01Z\ +eTVsOGt5SWdwaExkdTR1WWRpc0RPMG1ianEySE55K3pEakpyYXFHClNWTkZBZ01CQUFHalF6QkJNQjB\ +HQTFVZERnUVdCQlI1OG01YnpDaGlncmluaERETUJqSVJrWXovbURBUEJnTlYKSFJNQkFmOEVCVEFEQV\ +FIL01BOEdBMVVkRHdFQi93UUZBd01IQmdBd0RRWUpLb1pJaHZjTkFRRU1CUUFEZ2dJQgpBRWFDblluY\ +m9yWmRhWWZRUmxSb0dtSE94T1g5VFdNMXY0dWEweCtFcG11RDVWRDVlWEY1QVZkMlZadEdaeHYvCkd2\ +VnFBU2l0UTk3ampKV2p2bURWTUZtb3hQSmRYWDFkYTd5cmJYeFRmYm1mM1pac1RpdmZVdWQxYThxdUN\ +3dTUKTnBrdHFjV0JyMHRnNzFhOXlidHJOdm1GczZGRE1WMXkrY2JxYlp1UWlDWnc0WmZhekFaeTRQRG\ +NocE9SNjRCSwpBWFZIT05HcWFoV2hwcDkwd2E0TFEwUTE1U3FDR25kYVI4SHg5MHJOeEdkRjM1T1BLU\ +jd1TDdrWDU4ZGxaWStDClJJK1pKMndYMzJUZzgrc1RtTmNaUTliWDdvS0t3R3E5Uk94SjZJSUhOSnN2\ +bXhaSnlPcmE2N21hNTd3OWxiQnUKSzJlQ3cwZjRDeHdLNU1LNStkbWx6R3dhZmJlMG00TTBvVjlhWUx\ +ZTzAwR3Iza05heW9PdjRRVGtEM2pCMzVSMQpDMGJnQmk2eU1sTVJ2akZ5eEZkOFJpL003VG1jcXNObT\ +B5aklqbkZaenVtMFZTR1NLMXlRU3Flak40S0Y2R0JMCllpZ2JpM3c4WG5HYm9pZGdBUE9ncVVJeTJ1d\ +EU1MllzVXFsVHVncXhtM2xDOUhzaDM2UFJLNURDUG93eHVUNlgKcXo1M1ZiN2h6TkxLelpiRlJzbUdF\ +OFY2cWM2bXZTbUFXa25nL3QwaStXVmdGVkZuZFQrQ0oyNTJsa0ZacGljdAp6ekdETW44VUNDRUp4TDR\ +KTklTM2lLOUhlRys2MlZuay9QOEM3YVpLSXpVdjFud25rcVdUUUFYWDBKckJGdDdICjI5ZDk1RElmRT\ +RuT0FyS0JFNHc2Z1R4SU1uZzVzWi9ZbDFjcG5wUHlsR3VICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tL\ +S0K") + ("ethKey" . "fN8cOT1lYNziaW0+pjBIgZ8r6+zMMhHsukkWBNPDsFo=") + ("TURN.username" . "ring") + ("TURN.server" . "turn.jami.net") + ("TURN.realm" . "ring") + ("TURN.password" . "ring") + ("TURN.enable" . "true") + ("TLS.verifyServer" . "true") + ("TLS.verifyClient" . "true") + ("TLS.serverName" . "") + ("TLS.requireClientCertificate" . "true") + ("TLS.privateKeyFile" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQU\ +RBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzM5b1Z0cXNtUGdaSUgKcHpTV\ +GtlT3BlWC9CSEx2KzFTYnJPSFpVRHEwNFZCUU5BNmJmSFNSWTJpbHE1WEVheXNVSmwzQmsvM0txZEhS\ +cQpEV01wQ1dpcE1Vc2FwSGxJR0tSWHEwbXhQZ29WODZSUVBub1dCRTdhWVVEZTlJZXlxMmllZXpDK1l\ +YSnBtWTljCk5tblpaMFlHOHJGMEVpWFA0SHpVWGphZklTKzdKTTJ5ZTZyUlpINXBvdHBQNmV4NXhqVU\ +VuNEFFdWhuWGJ6U0EKNnZMaC9wYnhIUVBHMVlmNHFhcU9TY1lXV0ZWaE5ySVBCVFJNZ2RaTWRGRTh0W\ +VFyaUNkNTV4dUhrYncrQmY1VQpmVENqN25tVEcybDJNbGcrSXBHVkFXUFRWNkl0NFNiS3VadW5MZmRD\ +S2FrSU03SnA3V2dJWjZNRHdkaFFQWjJNClJPQzN0b0c1ajh4aFZOU0Q5QlZiOGZBcnRmeHdldlliQk1\ +yOEdRV2JQYlRKYk9tZHZnUGRGcXNTb1F4QnRUcWQKSG1TMG53RjJ4UGZrdm1Cd3BSWWhHNHpuZlVtMl\ +BQcDBIK1g5Sk96d0R1QWI5K0c0cWZtK3Z6bDd1N0o3anJ0TQpHQlJlSlREdGFNd2RHa0lmcUNRZGREZ\ +HFpM1hzVHVVOVNubVN4NkdxT2V6ZU04bk55dmtZWjR2Uk9vODFzU1JjCjQ4L3pZZjJ1OFBJUm4vVy9x\ +Zk9hbjVIbkcxeFNBUDZkcHAzQ1Y0YTgwUFduZyszcXQ4NW5Ha2k2Nk00NUlnRjgKNHZwRGUyM2YyZnp\ +rTk5OOTk1VTBvUStKVGg5eG1HaWJmenlCaXk5d3RUVHVYWDB3Y3gwTm9wNjdYaFFDVVBGSApybVd6Wj\ +V5bXhEdkRhcUN1U3NwcG0vTkRVRWQyc1FJREFRQUJBb0lDQUYyWXo4bzhXdERvMjZPSkx2Ym1BeTcyC\ +jRra2VsWWZTYXpyQ1AzSUZCWnpqS2xCMHl6STVZWVRUZXI4b2ZhTmtCMXdaOE5WeUlxVVhHeVBhSzls\ +MU1BcmwKU1pFRW5iQlEyVXpCbVh6VVU4MVhhUUpxeHpMc2ZqSWR5U09teG1QaFVobFFGRHpJMTYxaXM\ +4MzI0V1A3WjJXaApsU2U1RkFQdjg1TVpYREVhY1c2R0N5SUVTYVMvdkpHQ2loQ2VzL0pCSmpoejdtNT\ +VRU3liSjl0dnJxUE5KSDhJCmhDTm1BUWhEU3NPYVJXNlpBdGV6UEdUb0FQUHNHMXhLMGdwUlVDM1YyR\ +C90bGRRa0VlSjhxaW5TaUN6ZjZIc3cKTnpncjVUbTMzTm96R3Rjc2Z4ZFl0cVB1UzRPRG40bktLSFpE\ +MTBLTng2Qi9HakdQTHIra21jUUVSMUV4U2s2QwpSemtzSTRzRml6VnVLYTFNK2ZqaVNVc3RTdHV1cTJ\ +QQTJJNzFNTTZlMHJmU3ZKM3VESEJYOWY5T1RFaGxZUFBkCnB1VWM2ek1pdEJKRTg2SFo1M08wR0MzdG\ +p1M1BnSnhmWnJyMHBCU20rSzQ3ajVTUFNkaytiVGh3UDZDZDhzWGUKNmljS3YvMXI0SjVqd1pFTmRna\ +2hKeHVuMkE1WVA0c0NBU21JSFFXWmU1cGdFQ0ljUXBHZjNBVllVdVpGeXZ6cApLL3VBRTc0L3NMK0VU\ +UnhDUlhkN0ZJM2cwZFVySTA2WUFIV1FkY1JpdWpIUU12ZXB3V2RrdUdkaE9wV3VRTy9oCjc0MGgvRlZ\ +0dFdOMjc3ajhhc3ptSDA3T2lRWFIwTU1mN2FEaldMaElMYkd2YkV4TzN0TnRBYXZ6cGxmMUNSNk8KUm\ +1nNDdhVTZpR3lON1MwTE85RUJBb0lCQVFEWWMwelh2UmJHTWxkVVZnOFFTT1ZHKzV4NXVocTVyQ2xtQ\ +lp2ZApvYitWS1hkMXBONThraWkxTFBiYUJmZG9JcWVUamdOK0E0YnVpT3RGNjdCWUdISkp0WTk4ZWR5\ +RkpZREtCSzIyCmthRml2eWVuV01UWG5jaHMvUmNXMnVGS1M3cFAyNDRXYkF2clQxZGxmTTF3L1JzbTZ\ +QVjF1dndPSmNJcEdLSzgKWWgyN2hiaThUbEF6bnJCSS9TbGZwNlpxV3Y0MFRLanNNdmVGVFNWRU5yd1\ +F2MVl4TjcxbjA1UWRVUkIrT0lJSQpPenpNMWpNcm43cjNmS3RKTEx0RllEdUhJZzJDL2E0cUZORmpjM\ +296V1laQkhlcDkrWlEyOTgxdy96VTJIZ0oxCkxiajZjMy9qQU5EakI3MnRkcWMxVUkvMWhwVUh2WDJz\ +emY2czM0L3VSY3RlaFgxWEFvSUJBUURaazVaT01RdVMKNzlVQkhCeTRFbFR2cVkvUUxjMUd1aG9LVXE\ +0dzhONEtJZlpMSDk5TTcwek9JWjNoaEdaYWJDREZHN3RqSDl4Vwo3UnhXVWt5cFRDK1h5TTc1YjF0YW\ +9jRVVyZ1hnUnE4R1JFRXg3OUtwWUE3ei95TVc1OHJCTWhHTWlGZ1JTM00zCmNQSzg2dHBvaTFDN2crd\ +lUxcjJwcDEzN0habzZiUHpKTFRPNXA1OG1tTFRPQVpKd1VSZ3pzZWJnc1dsWXZlR0wKZWNBZ3lYbUtt\ +WmVSeGRNUFlCK0NmMno1TWZ5Ujk4MHRVSDJPaitGQjlJNExzRTExREphZ2wwS1lHMWxKRTBFagpodEV\ +1cTFOTG9DcVkzYXVKYjhtRUpqNlA2Ylc2SytCZTBlUi9CS1MraGxxb3VOYWEySnlhSDc4Qis2U1VZWE\ +RuCnd6MityWUhVZEI4M0FvSUJBRFlXc2ZnaloyS0Z4KzdxUm45aVIvRXlCUXNpSjNXSWdSdmVnUEdrY\ +nRTZWRSeXYKNDIwcnRRSjVSd0o2aFRXL216S3pSVW9qSlgvTU5VYld1ODEzNW05bThJRkJqb3F6TVhq\ +S0xJSzM1NlZlY1ZGUApUSGs1RTVHd3VTbGI3dnA2N0FieXJaSUswL3VzYXdHUWEySTF6YWd1aE5BenR\ +yTHVXcE9jZFdZditwQVd2WEJJCi9aKzRvd0xLU0tGL3FvVmZVYkRPQzFSaTlCbWFpcHArTndiVVdYeV\ +pHanFzMDVGejVYUTFPTUZIMUV5M3BqZmIKaFlRODRpeTZBZDQzU3dqY3lKV1lRUUtCQzBZWDRFeWVyW\ +Dd1TTkvaEUxbWRHUGlJdmNwVk8zWCt3Ly9LQndZNQorUGtTd1NKc3lTSDRqTkRsSGE2K2VuNUpSNy81\ +YWVVNENiY0lFcWNDZ2dFQkFLelMvNnhDWnZnalN5V2poK2hxCm4wN3plQW1icUJmTEVZNHJtTFBGVUF\ +ucWFqSElNbDV4SXFnRnFkd05pQ1BCQ2RLbnNaUU9KYjVpZjRUTndKa2wKckJRNzdMUFRVVlJQY2dnVU\ +p4Uzc4S0Rnckl5Vys5V1FPTEIxZEJEb3MzUDhhbFlmb3h5eHV1Wko4SFpCY3BWaQpQQkdHdTFnSDd3V\ +0lyUzBmbVhkWlJQNGp5cGRvM3hFUWNXWEZkK1dCZE9EektmcEcwZkFzZTdDSFdDWnpBdmttCkFYQklH\ +OXQxdGZHNWQvMEZTS05GbTVPb0FPT3h3L0xZNTgrL0RmZXd0U0VBcFdRZkxTL1BmSWxVdUdvQ3FwcEM\ +Kc2pOVXVNSGxxc011Z2JsY29mNHNoZityWjMzQldYOEJSNWdIb21mRE1ibDNDQWp5TXd1dHpybzVxcD\ +BBUTBWWApxOGNDZ2dFQkFLakFXVVRYY1F2TE8yYkxOZmJBTUlSVXk1T2lTZmJCbDNYRThSZnNzaUt2V\ +VBnTFcwSlV1V3FLCjdGdUFxTlJPRHhrS0pMSTdyQlo2YVNqNitFWHpUMnJwY2dFWktnSjFOUEFRdFNs\ +UjNJUkcyU3JJdjBFK3UzbkUKK1laa3pOa2Q0MUJqTkRjRm1HV21lZk5ROUJmaVIrZlZFSkZmcE5oSkl\ +mNUloSWU0RUtZUE5VUXNua0tSVTlxUApzWi9idXBXc2w4bWVFcko3bllJQ05ucHpnSHRpNXdSMlliVF\ +VXT01odmRFUldxMnhTV3BBYmtNMElhZDBUc05kCmUrYVRQVmJOMXFibFZLMm1qUTl2YS9JSkVuSE51V\ +E9TREtJeUpvcVArQkxiRTVjQU5acXQ2OFFadWdOc2RxNHkKV2FoeStydU5LS1F3Mk5MYzQzZUtsNmxv\ +bXdtRlFZOD0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=") + ("TLS.password" . "") + ("TLS.negotiationTimeoutSec" . "-1") + ("TLS.method" . "Automatic") + ("TLS.ciphers" . "") + ("TLS.certificateFile" . "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZHVENDQ\ +XdHZ0F3SUJBZ0lJU1pUdlZPQnh3akF3RFFZSktvWklodmNOQVFFTUJRQXdTVEVOTUFzR0ExVUUKQXhN\ +RVNtRnRhVEU0TURZR0NnbVNKb21UOGl4a0FRRVRLR1l6TXpRMVpqSTNOelZrWkdabE1EZGhOR0l3WkR\ +rMQpaR0ZsWVRFeE1XUXhOV1ppWXpFeE9Ua3dIaGNOTWpFd05ERTJNVGN6TWpFd1doY05NekV3TkRFME\ +1UY3pNakV3CldqQlFNUlF3RWdZRFZRUURFd3RLWVcxcElHUmxkbWxqWlRFNE1EWUdDZ21TSm9tVDhpe\ +GtBUUVUS0dFM09XTmwKTURVNE16VmhPRFV5WlRsbU5qWmxOelF3TURjeU5EUXdPVFZpTVdaa1kyVXdO\ +bVV3Z2dJaU1BMEdDU3FHU0liMwpEUUVCQVFVQUE0SUNEd0F3Z2dJS0FvSUNBUUMzOW9WdHFzbVBnWkl\ +IcHpTVGtlT3BlWC9CSEx2KzFTYnJPSFpVCkRxMDRWQlFOQTZiZkhTUlkyaWxxNVhFYXlzVUpsM0JrLz\ +NLcWRIUnFEV01wQ1dpcE1Vc2FwSGxJR0tSWHEwbXgKUGdvVjg2UlFQbm9XQkU3YVlVRGU5SWV5cTJpZ\ +WV6QytZWEpwbVk5Y05tblpaMFlHOHJGMEVpWFA0SHpVWGphZgpJUys3Sk0yeWU2clJaSDVwb3RwUDZl\ +eDV4alVFbjRBRXVoblhielNBNnZMaC9wYnhIUVBHMVlmNHFhcU9TY1lXCldGVmhOcklQQlRSTWdkWk1\ +kRkU4dFlRcmlDZDU1eHVIa2J3K0JmNVVmVENqN25tVEcybDJNbGcrSXBHVkFXUFQKVjZJdDRTYkt1Wn\ +VuTGZkQ0tha0lNN0pwN1dnSVo2TUR3ZGhRUFoyTVJPQzN0b0c1ajh4aFZOU0Q5QlZiOGZBcgp0Znh3Z\ +XZZYkJNcjhHUVdiUGJUSmJPbWR2Z1BkRnFzU29ReEJ0VHFkSG1TMG53RjJ4UGZrdm1Cd3BSWWhHNHpu\ +CmZVbTJQUHAwSCtYOUpPendEdUFiOStHNHFmbSt2emw3dTdKN2pydE1HQlJlSlREdGFNd2RHa0lmcUN\ +RZGREZHEKaTNYc1R1VTlTbm1TeDZHcU9lemVNOG5OeXZrWVo0dlJPbzgxc1NSYzQ4L3pZZjJ1OFBJUm\ +4vVy9xZk9hbjVIbgpHMXhTQVA2ZHBwM0NWNGE4MFBXbmcrM3F0ODVuR2tpNjZNNDVJZ0Y4NHZwRGUyM\ +2YyZnprTk5OOTk1VTBvUStKClRoOXhtR2liZnp5Qml5OXd0VFR1WFgwd2N4ME5vcDY3WGhRQ1VQRkhy\ +bVd6WjV5bXhEdkRhcUN1U3NwcG0vTkQKVUVkMnNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkRBVUFBNEl\ +DQVFCZmRCc0p4RVVWeFRBeG5vNll1bEFoR1ZvUAplWG8xMHIwOE5DcDZRZlJxeGJlTkZ5aXEvKzEwSE\ +NpL1RoZk41OVdLNlpudFF4NlFNZUxEUVZTb0NjNzlaaWQ4ClE2RUdsWkp1c2RTTmg0VjVteXRCQVZHZ\ +2J2aXJFWU1Wcm5jWWg3bHR2LzVuSGRsbyt2WXV3Vzh0aEhHTk1TUkIKQmhJN2xydmpqY3NkbVl6L1Bp\ +NFNZdkg3c3RaVWpRYmUzNzh3UE90b3lBMTNybEtQUTF4QjJFbUxISDYya21WTQowa25wL1hQQ2o2alR\ +zakpFWko1NzZNMmloYy9DTzkvREVlWWZ4RFlJb004NEF5dTc0UU9UUVN1cnVHRjF5T1RKCmlxRkltTH\ +hMcWRxNk1Zdmx1QjFmTzlKaU9QaS84UEJFTTVpeGYzajlGOFVMQTZUTU1NSklFR1ROeUNzOWpzQloKR\ +UNiWVgweTY4WWtGYnYzQmNWaGk2K1VVWVhBVUd0akhwQ3Z2VThYaHowL0RVZFVaTkpqejRJeWl1ZE5N\ +dnFjQgppSEJDdkxFS1B4VFd1ZlFZaGM5ZkVXTm1valc1WSsvalBVeDcrQWxPM1RXZm1OclBpWDJuSEd\ +5UG5tYjZIR2hWCmp3UU0wT1h4cGxnZ2dQMWpVZ0VYWWRucStjYzdRMy94RFIvdi9Fd244T1d4OVB3eV\ +Y0WFZwZXY5QnpmeWwyQTMKdFhWVkFKMzJtcTlPQmVadXczdWlWU2E2TWdCVG80eXcrYnBjU2VIU2xXO\ +XNRc1NYY3dUNjhvOUdhR2hMNjdXMApwQWx4R2VxSWRtbTZ0MkxIUCtQQnBtL3BweTMvTmh1NFIwMTNv\ +Znp5V2Y0bEcrVnpWVGE0eXVqU1J3UlluYmZyCjlSWER0L0I5VEg5TnNsamdBQT09Ci0tLS0tRU5EIEN\ +FUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ0F3SU\ +JBZ0lJRm1tNmZuaWRndEl3RFFZSktvWklodmNOQVFFTUJRQXdUREVRTUE0R0ExVUUKQXhNSFNtRnRhU\ +0JEUVRFNE1EWUdDZ21TSm9tVDhpeGtBUUVUS0RjNVpqSTJaVFZpWTJNeU9EWXlPREppT0dFMwpPRFF6\ +TUdOak1EWXpNakV4T1RFNFkyWm1PVGd3SGhjTk1qRXdOREUyTVRjek1qRXdXaGNOTXpFd05ERTBNVGN\ +6Ck1qRXdXakJKTVEwd0N3WURWUVFERXdSS1lXMXBNVGd3TmdZS0NaSW1pWlB5TEdRQkFSTW9aak16Tk\ +RWbU1qYzMKTldSa1ptVXdOMkUwWWpCa09UVmtZV1ZoTVRFeFpERTFabUpqTVRFNU9UQ0NBaUl3RFFZS\ +ktvWklodmNOQVFFQgpCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFQY0czaUQwUk84NE5XL2ZGVllWNHBLQ2My\ +ZE1EQXFCZVN2NEpCcTQ3OHQ5ClhjeERPeEhqSnYvR0dLRnFoVyt4cWFDOUJKZG44TGthdjBBTnMvV1d\ +3YXRNSDVsYlQrZlc0T2pGOVNCZ1Ivc2UKUlNPdWE1QncrSVJlSk5VSnR3enBJS3k1S1RhMnEwd0RyUX\ +hObXBuci9UK2NEK2RQSTJveUlFd2xwRHNKT2lVMAp6eStDU2pBb0FTYkh5TWcraGQwcWJiRFBUck1uR\ +2xKYkZubXdBQTBaRWU3aWg5Ymx0dlFPaU9TQWJVQm9IUHFpCmNtQW41RVFoNjVFa3dCb1p1U3hqanlC\ +VzZpWUJUbmc5S2c4U2pPVk9BYkJqaXZFZ3RyaFpYZ1JVdE11UWFmaDcKcjRRWDRuYjhRTUE5WXlhME8\ +xbFBJTUd5VEtiNHFIaE02VTd1RHV0NUFjcTN1NGFaZElPQ2prbUNFM3l4UU5UWAo5Vzl0b1NvV1l2YV\ +I1d1gxaFI5eVZCMFZ4V0N0djdZZklha1JiaWkxMS8xZHp3M1dXU1dMbTArTU4rY0pUdnluCk1EU29GN\ +jRpRmcycTVYd1VtWXpPRmJKQUtVd3M4QmVhcFRCVnRYaldDZmNKM01xZEZScDZvM25Lenlja0YwamoK\ +UjdLSmNRaWhJUW1sbXM1WEtzeHV1d0VMMmFKQ0o0Qm9uSUJDaXZWa09LbllsamtDeVNObzkzamlMalJ\ +4M0ZtRwpoTnJyT3lNbjFIcEd0Z1J2eFUxOC9Ob3FFaWtpdEhCeUtYNW5XU1lUUWN4MXlZY3kxcVo5cj\ +RqTmdQRzNuUHg0CjJEN2tPTlJPWXNVYXNBUlVTZHJqdlc5aXB2TDAvN2s1Yi9sRzh1WThPODN0NUFPS\ +TFVUG85Y05qMzNyeWJwbUgKQWdNQkFBR2pRekJCTUIwR0ExVWREZ1FXQkJUek5GOG5kZDMrQjZTdzJW\ +MnVvUkhSWDd3Um1UQVBCZ05WSFJNQgpBZjhFQlRBREFRSC9NQThHQTFVZER3RUIvd1FGQXdNSEJnQXd\ +EUVlKS29aSWh2Y05BUUVNQlFBRGdnSUJBRDMrCjlscFluMjZyeG5pekY2UkNvZFFFVmgvOVF2RWp1V3\ +dHUWZqa3gxb3VlVjdDMzUyWnpIT2hWU3VGcG43TUxkVFcKamI2dWhMRkpoMWtlTDlYZ0pHalMyV0Uwd\ +DlJUGp2UWx5UHIwRWJoWGRJNDJMYUR0NDk0dWQ2MEE0bWg0bW1zbwovcjY2NERKOWMwUjZBOUdQZUJj\ +Wi9zekhMQjJ4VmM5M3hYNjVjcmNoVTFLMDBVK2ZZUWtHS0xidFFXeUZzdnlKCmRHdFRxamVlYVVNeGt\ +5dFFWNWFxVHJ1SG4vV3U2RWRZejYrZ0ViQXJHUURRK1J3QmMxMDNGcWRIU0xReWdEWHoKd0pNVGhDRE\ +dBVmR6V2NCemJYL1JIZms5bTZzK01HblNJUFBrNG9FOUdFbGdQc2JUZ2FrTzc3aElUdzRxZXJiTAp2M\ +0tiaTVOeVZZVFNVa3hDb0VRSkIrWlZJT3BVNFhRdVAvNkZkWldBRk1LU0Jkd1JLcnRmT1hZT1ExcVBv\ +YU1uCjZPR1VGMU0rYWZ0dTNCMkNhZ2ZMaE5hbVBoSjdxWTNSMzJhK2VRVllvYWlESXZKMElIUHFnQ0N\ +uWXlIaVBHTjQKS3VvaE9IZFpXTkxOUXdIQTg3SDV5NEwrSGZISmdFZFppdWYrbTNMa3JJcnN5MWFoUj\ +gyaEpZVkhreHVyZVRDcQpIR2NJaUIvTHpkSG91WFVrWGNrNjRvWXVRQnM3ZE9KVEx6bGlibU8xelRzV\ +WRoc0d4dE9zc2lYWkFjUURmZHBnCjNHdFJ3UkRwd0ZtL2k5TC9UWjMzMjBwY0VZd21aN1dBSzJRa3Z6\ +eWZlelRGeXdLWmh3c0RQRkwycllzLzhXdWsKRk1sek5BVmRMTytTS1RxeGlkM0l1elFaQ0FwU05ybTJ\ +MY2ZVN2grWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS\ +0tLQpNSUlGWFRDQ0EwV2dBd0lCQWdJSXV0T3MxUXBsSVRvd0RRWUpLb1pJaHZjTkFRRU1CUUF3VERFU\ +U1BNEdBMVVFCkF4TUhTbUZ0YVNCRFFURTRNRFlHQ2dtU0pvbVQ4aXhrQVFFVEtEYzVaakkyWlRWaVky\ +TXlPRFl5T0RKaU9HRTMKT0RRek1HTmpNRFl6TWpFeE9URTRZMlptT1Rnd0hoY05NakV3TkRFMk1UY3p\ +NakExV2hjTk16RXdOREUwTVRjegpNakExV2pCTU1SQXdEZ1lEVlFRREV3ZEtZVzFwSUVOQk1UZ3dOZ1\ +lLQ1pJbWlaUHlMR1FCQVJNb056bG1NalpsCk5XSmpZekk0TmpJNE1tSTRZVGM0TkRNd1kyTXdOak15T\ +VRFNU1UaGpabVk1T0RDQ0FpSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0lCQUxT\ +cGduY2tYODd3OE5vVzdRWE5DbWtHSHphVnN1NE9UbjFSSUhqVQpGR0w5MjNvVlk5RHVoZjFtVnllamg\ +remVCOCszaTR4b1E5MU04WDdTVnk4SHZxejQ4c0I2djAxMk5RTGpwN08xCkd4dmc1a0REeDRqbmVyUW\ +d6d0JzVEFuaC8xNy9SOEFDZkhINzYzUjhMRjY1clJPSStOT21KNUV2cU9rRWJJOXYKb0UySko1czBVS\ +HZZM1hTQ2pid01ROXRkM2c0SG9LNmRYSU44a2d6ejg0Yi83YkF5TnZ4a09rNnVSei9CSjhhZQozWjZu\ +dElza2tmVHhIZFpBbGtpVW5DU21Bb3VwQ01TbGRLS3RMRzNja2FhN0FDMDRxS29Rekc0WTN2TlRCOUl\ +mCmdvYThOazlKMThoaFN2REVuR0haaURLR1dkQVA5RW5GRHZERnRFRFZyVVZSWUN5WHdyejMya29Na3\ +dYODVUMTgKc1F2TUZSdWRsa1BoOGxmbnFEWTFpZEd1R2IvSGlnU1ZqalJKZlVTbS9lZ3hycStSTnpJW\ +GFBOWNuOXAxQkdFbwpZU0YxeUdtTEgzSkJscUhVRTg4VnFkenZRdXZ3Mkhvakdvd0pkdlhmU2loZk5W\ +OTh6VkEzQ2o3L0d3V2w0VU9OCkRUVUk2SzUrR3Y0a01iYXdxcWduaFlHbHBlZVp2bHE2UlI3OVpNblR\ +WVjJ1RWo4aERYQzdFTGN2Yk5tekJSSkoKS0gzaUxIbmNlSkxOV2NIMUV0K0pkaEFRR2xEYmk2RVR0dH\ +RuaWVaN3ZkMyt6UE00eXpkb3JEMlJXUjRyWG1acQpGRlI5eTBab2hQVTBjVmdKZHFtdUpyV01ZeTVsO\ +Gt5SWdwaExkdTR1WWRpc0RPMG1ianEySE55K3pEakpyYXFHClNWTkZBZ01CQUFHalF6QkJNQjBHQTFV\ +ZERnUVdCQlI1OG01YnpDaGlncmluaERETUJqSVJrWXovbURBUEJnTlYKSFJNQkFmOEVCVEFEQVFIL01\ +BOEdBMVVkRHdFQi93UUZBd01IQmdBd0RRWUpLb1pJaHZjTkFRRU1CUUFEZ2dJQgpBRWFDblluYm9yWm\ +RhWWZRUmxSb0dtSE94T1g5VFdNMXY0dWEweCtFcG11RDVWRDVlWEY1QVZkMlZadEdaeHYvCkd2VnFBU\ +2l0UTk3ampKV2p2bURWTUZtb3hQSmRYWDFkYTd5cmJYeFRmYm1mM1pac1RpdmZVdWQxYThxdUN3dTUK\ +TnBrdHFjV0JyMHRnNzFhOXlidHJOdm1GczZGRE1WMXkrY2JxYlp1UWlDWnc0WmZhekFaeTRQRGNocE9\ +SNjRCSwpBWFZIT05HcWFoV2hwcDkwd2E0TFEwUTE1U3FDR25kYVI4SHg5MHJOeEdkRjM1T1BLUjd1TD\ +drWDU4ZGxaWStDClJJK1pKMndYMzJUZzgrc1RtTmNaUTliWDdvS0t3R3E5Uk94SjZJSUhOSnN2bXhaS\ +nlPcmE2N21hNTd3OWxiQnUKSzJlQ3cwZjRDeHdLNU1LNStkbWx6R3dhZmJlMG00TTBvVjlhWUxZTzAw\ +R3Iza05heW9PdjRRVGtEM2pCMzVSMQpDMGJnQmk2eU1sTVJ2akZ5eEZkOFJpL003VG1jcXNObTB5akl\ +qbkZaenVtMFZTR1NLMXlRU3Flak40S0Y2R0JMCllpZ2JpM3c4WG5HYm9pZGdBUE9ncVVJeTJ1dEU1Ml\ +lzVXFsVHVncXhtM2xDOUhzaDM2UFJLNURDUG93eHVUNlgKcXo1M1ZiN2h6TkxLelpiRlJzbUdFOFY2c\ +WM2bXZTbUFXa25nL3QwaStXVmdGVkZuZFQrQ0oyNTJsa0ZacGljdAp6ekdETW44VUNDRUp4TDRKTklT\ +M2lLOUhlRys2MlZuay9QOEM3YVpLSXpVdjFud25rcVdUUUFYWDBKckJGdDdICjI5ZDk1RElmRTRuT0F\ +yS0JFNHc2Z1R4SU1uZzVzWi9ZbDFjcG5wUHlsR3VICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K") + ("STUN.server" . "") + ("STUN.enable" . "false") + ("SRTP.rtpFallback" . "false") + ("SRTP.keyExchange" . "sdes") + ("SRTP.enable" . "true") + ("RingNS.uri" . "") + ("RingNS.account" . "0790738ce15fa05933b49dd77034312787da86c3") + ("DHT.PublicInCalls" . "true") + ("Account.videoPortMin" . "49152") + ("Account.videoPortMax" . "65534") + ("Account.videoEnabled" . "true") + ("Account.username" . "f3345f2775ddfe07a4b0d95daea111d15fbc1199") + ("Account.useragent" . "") + ("Account.upnpEnabled" . "true") + ("Account.type" . "RING") + ("Account.ringtoneEnabled" . "true") + ("Account.rendezVous" . "true") + ("Account.publishedSameAsLocal" . "true") + ("Account.publishedPort" . "5060") + ("Account.publishedAddress" . "") + ("Account.presenceSubscribeSupported" . "true") + ("Account.peerDiscovery" . "false") + ("Account.managerUsername" . "") + ("Account.managerUri" . "") + ("Account.mailbox" . "") + ("Account.localModeratorsEnabled" . "true") + ("Account.localInterface" . "default") + ("Account.hostname" . "bootstrap.jami.net") + ("Account.hasCustomUserAgent" . "false") + ("Account.enable" . "true") + ("Account.dtmfType" . "overrtp") + ("Account.displayName" . "dummy") + ("Account.defaultModerators" . "") + ("Account.audioPortMin" . "16384") + ("Account.audioPortMax" . "32766") + ("Account.archiveHasPassword" . "false") + ("Account.allowCertFromTrusted" . "true") + ("Account.allowCertFromHistory" . "true") + ("Account.allowCertFromContact" . "true") + ("Account.allModeratorEnabled" . "true") + ("Account.alias" . "dummy") + ("Account.activeCallLimit" . "-1") + ("Account.accountPublish" . "false") + ("Account.accountDiscovery" . "false"))) diff --git a/gnu/tests/telephony.scm b/gnu/tests/telephony.scm new file mode 100644 index 0000000000..1155a9dbc2 --- /dev/null +++ b/gnu/tests/telephony.scm @@ -0,0 +1,366 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Maxim Cournoyer . +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix 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 General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (gnu tests telephony) + #:use-module (gnu) + #:use-module (gnu packages) + #:use-module (gnu packages guile) + #:use-module (gnu tests) + #:use-module (gnu system vm) + #:use-module (gnu services) + #:use-module (gnu services dbus) + #:use-module (gnu services networking) + #:use-module (gnu services ssh) + #:use-module (gnu services telephony) + #:use-module (guix gexp) + #:use-module (guix modules) + #:export (%test-jami + %test-jami-provisioning)) + +;;; +;;; Jami daemon. +;;; + +(include "data/jami-dummy-account.dat") ;defines %jami-account-content-sexp + +(define %dummy-jami-account-archive + ;; A Jami account archive is a gzipped JSON file. + (computed-file + "dummy-jami-account.gz" + (with-extensions (list guile-json-4 guile-zlib) + #~(begin + (use-modules (json) (zlib)) + (let ((port (open-output-file #$output))) + (call-with-gzip-output-port port + (lambda (port) + (scm->json '#$%jami-account-content-sexp port)))))))) + +(define %allowed-contacts '("1dbcb0f5f37324228235564b79f2b9737e9a008f" + "2dbcb0f5f37324228235564b79f2b9737e9a008f")) + +(define %moderators '("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + +(define %dummy-jami-account (jami-account + (archive %dummy-jami-account-archive) + (allowed-contacts %allowed-contacts) + (moderators %moderators) + (rendezvous-point? #t) + (peer-discovery? #f) + (bootstrap-hostnames '("bootstrap.me" + "fallback.another.host")) + (name-server-uri "https://my.name.server"))) + +(define* (make-jami-os #:key provisioning?) + (operating-system + (host-name "jami") + (timezone "America/Montreal") + (locale "en_US.UTF-8") + + (bootloader (bootloader-configuration + (bootloader grub-bootloader) + (target "/dev/sdX"))) + (file-systems (cons (file-system + (device (file-system-label "my-root")) + (mount-point "/") + (type "ext4")) + %base-file-systems)) + (firmware '()) + + (services (cons* (service jami-service-type + (if provisioning? + (jami-configuration + (debug? #t) + (accounts (list %dummy-jami-account))) + (jami-configuration + (debug? #t)))) + (service dbus-root-service-type) + ;; The following services/packages are added for + ;; debugging purposes. + (service dhcp-client-service-type) + (service openssh-service-type + (openssh-configuration + (permit-root-login #t) + (allow-empty-passwords? #t))) + %base-services)) + (packages (cons* (specification->package "recutils") + (specification->package "strace") + %base-packages)))) + +(define %jami-os + (make-jami-os)) + +(define %jami-os-provisioning + (make-jami-os #:provisioning? #t)) + +(define* (run-jami-test #:key provisioning?) + "Run tests in %JAMI-OS. When PROVISIONING? is true, test the +accounts provisioning feature of the service." + (define os (marionette-operating-system + (if provisioning? + %jami-os-provisioning + %jami-os) + #:imported-modules '((gnu services herd) + (guix combinators)))) + (define vm (virtual-machine + (operating-system os) + (memory-size 512))) + + (define username (assoc-ref %jami-account-content-sexp + "Account.username")) + + (define test + (with-imported-modules (source-module-closure + '((gnu build marionette) + (gnu build jami-service))) + #~(begin + (use-modules (rnrs base) + (srfi srfi-11) + (srfi srfi-64) + (gnu build marionette) + (gnu build jami-service)) + + (define marionette + (make-marionette (list #$vm))) + + (mkdir #$output) + (chdir #$output) + + (test-begin "jami") + + (test-assert "service is running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) (number? pid)))))) + marionette)) + + (test-assert "service can be stopped" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + (setenv "PATH" "/run/current-system/profile/bin") + (let ((pid (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) pid)))))) + + (assert (number? pid)) + + (match (stop-service 'jami) + (services ;a list of service symbols + (member 'jami services))) + ;; Sometimes, the process still appear in pgrep, even + ;; though we are using waitpid after sending it SIGTERM + ;; in the service; use retries. + (with-retries 20 1 + (not (zero? (status:exit-val + (system* "pgrep" "dring"))))))) + marionette)) + + (test-assert "service can be restarted" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + ;; Start and retrieve the current PID. + (define pid (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) pid))))) + (assert (number? pid)) + + ;; Restart the service. + (restart-service 'jami) + + (define new-pid (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) pid))))) + (assert (number? new-pid)) + + (not (eq? pid new-pid))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami accounts provisioning, account present" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + ;; Accounts take some time to appear after being added. + (with-retries 20 1 + (with-shepherd-action 'jami ('list-accounts) results + (let ((account (assoc-ref (car results) #$username))) + (assert (string=? #$username + (assoc-ref account + "Account.username"))))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami accounts provisioning, allowed-contacts" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + ;; Public mode is disabled. + (with-shepherd-action 'jami ('list-account-details) + results + (let ((account (assoc-ref (car results) #$username))) + (assert (string=? "false" + (assoc-ref account + "DHT.PublicInCalls"))))) + + ;; Allowed contacts match those declared in the configuration. + (with-shepherd-action 'jami ('list-contacts) results + (let ((contacts (assoc-ref (car results) #$username))) + (assert (lset= string-ci=? contacts '#$%allowed-contacts))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami accounts provisioning, moderators" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + ;; Moderators match those declared in the configuration. + (with-shepherd-action 'jami ('list-moderators) results + (let ((moderators (assoc-ref (car results) #$username))) + (assert (lset= string-ci=? moderators '#$%moderators)))) + + ;; Moderators can be added via the Shepherd action. + (with-shepherd-action 'jami + ('add-moderator "cccccccccccccccccccccccccccccccccccccccc" + #$username) results + (let ((moderators (car results))) + (assert (lset= string-ci=? moderators + (cons "cccccccccccccccccccccccccccccccccccccccc" + '#$%moderators)))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami service actions, ban/unban contacts" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + ;; Globally ban a contact. + (with-shepherd-action 'jami + ('ban-contact "1dbcb0f5f37324228235564b79f2b9737e9a008f") _ + (with-shepherd-action 'jami ('list-banned-contacts) results + (every (match-lambda + ((username . banned-contacts) + (member "1dbcb0f5f37324228235564b79f2b9737e9a008f" + banned-contacts))) + (car results)))) + + ;; Ban a contact for a single account. + (with-shepherd-action 'jami + ('ban-contact "dddddddddddddddddddddddddddddddddddddddd" + #$username) _ + (with-shepherd-action 'jami ('list-banned-contacts) results + (every (match-lambda + ((username . banned-contacts) + (let ((found? (member "dddddddddddddddddddddddddddddddddddddddd" + banned-contacts))) + (if (string=? #$username username) + found? + (not found?))))) + (car results))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami service actions, enable/disable accounts" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + + (with-shepherd-action 'jami + ('disable-account #$username) _ + (with-shepherd-action 'jami ('list-accounts) results + (let ((account (assoc-ref (car results) #$username))) + (assert (string= "false" + (assoc-ref account "Account.enable")))))) + + (with-shepherd-action 'jami + ('enable-account #$username) _ + (with-shepherd-action 'jami ('list-accounts) results + (let ((account (assoc-ref (car results) #$username))) + (assert (string= "true" + (assoc-ref account "Account.enable"))))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami account parameters" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + (with-shepherd-action 'jami ('list-account-details) results + (let ((account-details (assoc-ref (car results) + #$username))) + (assert (lset<= + equal? + '(("Account.hostname" . + "bootstrap.me;fallback.another.host") + ("Account.peerDiscovery" . "false") + ("Account.rendezVous" . "true") + ("RingNS.uri" . "https://my.name.server")) + account-details))))) + marionette)) + + (test-end) + (exit (= (test-runner-fail-count (test-runner-current)) 0))))) + + (gexp->derivation (if provisioning? + "jami-provisioning-test" + "jami-test") + test)) + +(define %test-jami + (system-test + (name "jami") + (description "Basic tests for the jami service.") + (value (run-jami-test)))) + +(define %test-jami-provisioning + (system-test + (name "jami-provisioning") + (description "Provisioning test for the jami service.") + (value (run-jami-test #:provisioning? #t)))) + +;; Local Variables: +;; eval: (put 'with-retries 'scheme-indent-function 2) +;; End: diff --git a/tests/services/telephony.scm b/tests/services/telephony.scm new file mode 100644 index 0000000000..b4a0f120d4 --- /dev/null +++ b/tests/services/telephony.scm @@ -0,0 +1,446 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Maxim Cournoyer +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix 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 General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (tests services telephony) + #:use-module (gnu build jami-service) + #:use-module (gnu services telephony) + #:use-module (srfi srfi-64)) + +;;; Tests for the (gnu services telephony) and related modules. + +(test-begin "jami-service") + +(define parse-dbus-reply + (@@ (gnu build jami-service) parse-dbus-reply)) + +(define parse-account-ids + (@@ (gnu build jami-service) parse-account-ids)) + +(define parse-account-details + (@@ (gnu build jami-service) parse-account-details)) + +(define parse-contacts + (@@ (gnu build jami-service) parse-contacts)) + +(define jami-account->alist + (@@ (gnu services telephony) jami-account->alist)) + +;; $ dbus-send --print-reply --dest="cx.ring.Ring" \ +;; "/cx/ring/Ring/ConfigurationManager" \ +;; "cx.ring.Ring.ConfigurationManager.getAccountList" +(define getAccountList-reply "\ +method return time=1622217253.386711 sender=:1.7 -> destination=:1.14 serial=140 reply_serial=2 + array [ + string \"addf37fbb558d6a0\" + string \"d5cbeb7d08c98a65\" + string \"398af0c6b74ce101\" + ] +") + +(test-equal "parse-account-ids" + '("addf37fbb558d6a0" "d5cbeb7d08c98a65" "398af0c6b74ce101") + (parse-account-ids getAccountList-reply)) + +;; $ dbus-send --print-reply --dest="cx.ring.Ring" \ +;; "/cx/ring/Ring/ConfigurationManager" \ +;; "cx.ring.Ring.ConfigurationManager.getAccountDetails" \ +;; 'string:398af0c6b74ce101' +(define getAccountDetails-reply "\ +method return time=1622254991.789588 sender=:1.7 -> destination=:1.19 serial=145 reply_serial=2 + array [ + dict entry( + string \"Account.accountDiscovery\" + string \"false\" + ) + dict entry( + string \"Account.accountPublish\" + string \"false\" + ) + dict entry( + string \"Account.activeCallLimit\" + string \"-1\" + ) + dict entry( + string \"Account.alias\" + string \"some-rendezvous-point-name\" + ) + dict entry( + string \"Account.allModeratorEnabled\" + string \"true\" + ) + dict entry( + string \"Account.allowCertFromContact\" + string \"true\" + ) + dict entry( + string \"Account.allowCertFromHistory\" + string \"true\" + ) + dict entry( + string \"Account.allowCertFromTrusted\" + string \"true\" + ) + dict entry( + string \"Account.archiveHasPassword\" + string \"false\" + ) + dict entry( + string \"Account.audioPortMax\" + string \"32766\" + ) + dict entry( + string \"Account.audioPortMin\" + string \"16384\" + ) + dict entry( + string \"Account.autoAnswer\" + string \"false\" + ) + dict entry( + string \"Account.defaultModerators\" + string \"\" + ) + dict entry( + string \"Account.deviceID\" + string \"94b4070fc7a8afa8482c777a9822c52e6af2e1bd\" + ) + dict entry( + string \"Account.deviceName\" + string \"some-device\" + ) + dict entry( + string \"Account.dhtProxyListUrl\" + string \"https://config.jami.net/proxyList\" + ) + dict entry( + string \"Account.displayName\" + string \"some-rendezvous-point-name\" + ) + dict entry( + string \"Account.dtmfType\" + string \"overrtp\" + ) + dict entry( + string \"Account.enable\" + string \"true\" + ) + dict entry( + string \"Account.hasCustomUserAgent\" + string \"false\" + ) + dict entry( + string \"Account.hostname\" + string \"bootstrap.jami.net\" + ) + dict entry( + string \"Account.localInterface\" + string \"default\" + ) + dict entry( + string \"Account.localModeratorsEnabled\" + string \"true\" + ) + dict entry( + string \"Account.mailbox\" + string \"\" + ) + dict entry( + string \"Account.managerUri\" + string \"\" + ) + dict entry( + string \"Account.managerUsername\" + string \"\" + ) + dict entry( + string \"Account.peerDiscovery\" + string \"false\" + ) + dict entry( + string \"Account.presenceSubscribeSupported\" + string \"true\" + ) + dict entry( + string \"Account.proxyEnabled\" + string \"false\" + ) + dict entry( + string \"Account.proxyPushToken\" + string \"\" + ) + dict entry( + string \"Account.proxyServer\" + string \"dhtproxy.jami.net:[80-95]\" + ) + dict entry( + string \"Account.publishedAddress\" + string \"\" + ) + dict entry( + string \"Account.publishedPort\" + string \"5060\" + ) + dict entry( + string \"Account.publishedSameAsLocal\" + string \"true\" + ) + dict entry( + string \"Account.rendezVous\" + string \"true\" + ) + dict entry( + string \"Account.ringtoneEnabled\" + string \"true\" + ) + dict entry( + string \"Account.ringtonePath\" + string \"/usr/share/ring/ringtones/default.opus\" + ) + dict entry( + string \"Account.type\" + string \"RING\" + ) + dict entry( + string \"Account.upnpEnabled\" + string \"true\" + ) + dict entry( + string \"Account.useragent\" + string \"\" + ) + dict entry( + string \"Account.username\" + string \"ccb8bbe2382343f7feb140710ab48aaf1b55634e\" + ) + dict entry( + string \"Account.videoEnabled\" + string \"true\" + ) + dict entry( + string \"Account.videoPortMax\" + string \"65534\" + ) + dict entry( + string \"Account.videoPortMin\" + string \"49152\" + ) + dict entry( + string \"DHT.PublicInCalls\" + string \"true\" + ) + dict entry( + string \"DHT.port\" + string \"7766\" + ) + dict entry( + string \"RingNS.account\" + string \"3989b55313a911b6f0c004748b49b254f35c9ef6\" + ) + dict entry( + string \"RingNS.uri\" + string \"\" + ) + dict entry( + string \"SRTP.enable\" + string \"true\" + ) + dict entry( + string \"SRTP.keyExchange\" + string \"sdes\" + ) + dict entry( + string \"SRTP.rtpFallback\" + string \"false\" + ) + dict entry( + string \"STUN.enable\" + string \"false\" + ) + dict entry( + string \"STUN.server\" + string \"\" + ) + dict entry( + string \"TLS.certificateFile\" + string \"/var/lib/jami/.local/share/jami/398af0c6b74ce101/ring_device.crt\" + ) + dict entry( + string \"TLS.certificateListFile\" + string \"\" + ) + dict entry( + string \"TLS.ciphers\" + string \"\" + ) + dict entry( + string \"TLS.method\" + string \"Automatic\" + ) + dict entry( + string \"TLS.negotiationTimeoutSec\" + string \"-1\" + ) + dict entry( + string \"TLS.password\" + string \"\" + ) + dict entry( + string \"TLS.privateKeyFile\" + string \"/var/lib/jami/.local/share/jami/398af0c6b74ce101/ring_device.key\" + ) + dict entry( + string \"TLS.requireClientCertificate\" + string \"true\" + ) + dict entry( + string \"TLS.serverName\" + string \"\" + ) + dict entry( + string \"TLS.verifyClient\" + string \"true\" + ) + dict entry( + string \"TLS.verifyServer\" + string \"true\" + ) + dict entry( + string \"TURN.enable\" + string \"true\" + ) + dict entry( + string \"TURN.password\" + string \"ring\" + ) + dict entry( + string \"TURN.realm\" + string \"ring\" + ) + dict entry( + string \"TURN.server\" + string \"turn.jami.net\" + ) + dict entry( + string \"TURN.username\" + string \"ring\" + ) + ] +") + +(test-equal "parse-account-details; username, alias and display name" + '("ccb8bbe2382343f7feb140710ab48aaf1b55634e" ;username + "some-rendezvous-point-name" ;alias + "some-rendezvous-point-name") ;displayName + (let ((account-details (parse-account-details getAccountDetails-reply))) + (list (assoc-ref account-details "Account.username") + (assoc-ref account-details "Account.alias") + (assoc-ref account-details "Account.displayName")))) + +(define getContacts-reply "\ +method return time=1627014042.752673 sender=:1.113 -> destination=:1.186 serial=220 reply_serial=2 + array [ + array [ + dict entry( + string \"added\" + string \"1578883327\" + ) + dict entry( + string \"confirmed\" + string \"true\" + ) + dict entry( + string \"id\" + string \"1c7d5a09464223442549fef172a3cf6f4de9b01c\" + ) + ] + array [ + dict entry( + string \"added\" + string \"1623107941\" + ) + dict entry( + string \"confirmed\" + string \"true\" + ) + dict entry( + string \"id\" + string \"5903c6c9ac5cb863c64e559add3d5d1c8c563449\" + ) + ] + array [ + dict entry( + string \"added\" + string \"1595996256\" + ) + dict entry( + string \"confirmed\" + string \"true\" + ) + dict entry( + string \"id\" + string \"ff2d72a548693214fb3a0f0f7a943b5e2bb9be03\" + ) + ] + ]") + +(test-equal "parse-account-contacts" + '((("added" . "1578883327") + ("confirmed" . "true") + ("id" . "1c7d5a09464223442549fef172a3cf6f4de9b01c")) + (("added" . "1623107941") + ("confirmed" . "true") + ("id" . "5903c6c9ac5cb863c64e559add3d5d1c8c563449")) + (("added" . "1595996256") + ("confirmed" . "true") + ("id" . "ff2d72a548693214fb3a0f0f7a943b5e2bb9be03"))) + (parse-contacts getContacts-reply)) + +(define getContacts-empty-reply "\ +method return time=1627400787.873988 sender=:1.1197 -> destination=:1.1463 serial=2127 reply_serial=2 + array [ + ]") + +(test-equal "parse-account-contacts, empty array" + '() + (parse-contacts getContacts-empty-reply)) + +(define %dummy-jami-account (jami-account + (archive "/tmp/dummy.gz"))) + +(define %dummy-jami-account-2 (jami-account + (archive "/tmp/dummy.gz") + (rendezvous-point? #t) + (peer-discovery? #f) + (bootstrap-hostnames '("bootstrap.me" + "fallback.another.host")) + (name-server-uri "https://my.name.server"))) + +(test-equal "jami-account->alist, no account detail value set" + '() + (jami-account->alist %dummy-jami-account)) + +(test-equal "jami-account->alist, with account detail values" + '(("Account.hostname" . "bootstrap.me;fallback.another.host") + ("Account.peerDiscovery" . "false") + ("Account.rendezVous" . "true") + ("RingNS.uri" . "https://my.name.server")) + (sort (jami-account->alist %dummy-jami-account-2) + (lambda (x y) + (string<=? (car x) (car y))))) + +(test-end) -- cgit v1.2.3 From 4673f817938d9d2b1b40a072ab2e0c44a32ccc97 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 2 Aug 2021 15:48:59 -0400 Subject: Revert "services: Add a service for Jami." This reverts commit 69dcc24c9f0cdfea674eb690e7755d26a25ced2b. It broke 'guix pull'. --- Makefile.am | 1 - doc/guix.texi | 228 ------------ gnu/build/jami-service.scm | 587 ----------------------------- gnu/local.mk | 5 +- gnu/services/telephony.scm | 684 +--------------------------------- gnu/tests/data/jami-dummy-account.dat | 392 ------------------- gnu/tests/telephony.scm | 366 ------------------ tests/services/telephony.scm | 446 ---------------------- 8 files changed, 3 insertions(+), 2706 deletions(-) delete mode 100644 gnu/build/jami-service.scm delete mode 100644 gnu/tests/data/jami-dummy-account.dat delete mode 100644 gnu/tests/telephony.scm delete mode 100644 tests/services/telephony.scm (limited to 'gnu/build') diff --git a/Makefile.am b/Makefile.am index 5542aa1c56..d5ec909213 100644 --- a/Makefile.am +++ b/Makefile.am @@ -489,7 +489,6 @@ SCM_TESTS = \ tests/services/file-sharing.scm \ tests/services/configuration.scm \ tests/services/linux.scm \ - tests/services/telephony.scm \ tests/sets.scm \ tests/size.scm \ tests/status.scm \ diff --git a/doc/guix.texi b/doc/guix.texi index 9a9c85678c..2298d512a1 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -22526,234 +22526,6 @@ and Error. @node Telephony Services @subsection Telephony Services -@cindex telephony, services -The @code{(gnu services telephony)} module contains Guix service -definitions for telephony services. Currently it provides the following -services: - -@subsubheading Jami - -@cindex jami, service - -This section describes how to configure a Jami server that can be used -to host video (or audio) conferences, among other uses. The following -example demonstrates how to specify Jami account archives (backups) to -be provisioned automatically: - -@lisp -(service jami-service-type - (jami-configuration - (accounts - (list (jami-account - (archive "/etc/jami/unencrypted-account-1.gz")) - (jami-account - (archive "/etc/jami/unencrypted-account-2.gz")))))) -@end lisp - -When the accounts field is specified, the Jami account files of the -service found under @file{/var/lib/jami} are recreated every time the -service starts. - -Jami accounts and their corresponding backup archives can be generated -using either the @code{jami-qt} or @code{jami-gnome} Jami clients. The -accounts should not be password-protected, but it is wise to ensure -their files are only readable by @samp{root}. - -The next example shows how to declare that only some contacts should be -allowed to communicate with a given account: - -@lisp -(service jami-service-type - (jami-configuration - (accounts - (list (jami-account - (archive "/etc/jami/unencrypted-account-1.gz") - (peer-discovery? #t) - (rendezvous-point? #t) - (allowed-contacts - '("1dbcb0f5f37324228235564b79f2b9737e9a008f" - "2dbcb0f5f37324228235564b79f2b9737e9a008f"))))))) -@end lisp - -In this mode, only the declared @code{allowed-contacts} can initiate -communication with the Jami account. This can be used, for example, -with rendezvous point accounts to create a private video conferencing -space. - -To put the system administrator in full control of the conferences -hosted on their system, the Jami service supports the following actions: - -@example sh -# herd doc jami list-actions jami -(list-accounts - list-account-details - list-banned-contacts - list-contacts - list-moderators - add-moderator - ban-contact - enable-account - disable-account) -@end example - -The above actions aim to provide the most valuable actions for -moderation purposes, not to cover the whole Jami API. Users wanting to -interact with the Jami daemon from Guile may be interested in -experimenting with the @code{(gnu build jami-service)} module, which -powers the above Shepherd actions. - -@c TODO: This should be auto-generated from the doc already defined on -@c the shepherd-actions themselves in (gnu services telephony). -The @code{add-moderator} and @code{ban-contact} actions accept a contact -@emph{fingerprint} (40 characters long hash) as first argument and an -account fingerprint or username as second argument: - -@example sh -# herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f \ - f3345f2775ddfe07a4b0d95daea111d15fbc1199 - -# herd list-moderators jami -Moderators for account f3345f2775ddfe07a4b0d95daea111d15fbc1199: - - 1dbcb0f5f37324228235564b79f2b9737e9a008f - -@end example - -In the case of @code{ban-contact}, the second username argument is -optional; when omitted, the account is banned from all Jami accounts: - -@example sh -# herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f - -# herd list-banned-contacts jami -Banned contacts for account f3345f2775ddfe07a4b0d95daea111d15fbc1199: - - 1dbcb0f5f37324228235564b79f2b9737e9a008f - -@end example - -Banned contacts are also stripped from their moderation privileges. - -The @code{disable-account} action allows to completely disconnect an -account from the network, making it unreachable, while -@code{enable-account} does the inverse. They accept a single account -username or fingerprint as first argument: - -@example sh -# herd disable-account jami f3345f2775ddfe07a4b0d95daea111d15fbc1199 - -# herd list-accounts jami -The following Jami accounts are available: - - f3345f2775ddfe07a4b0d95daea111d15fbc1199 (dummy) [disabled] - -@end example - -The @code{list-account-details} action prints the detailed parameters of -each accounts in the Recutils format, which means the @command{recsel} -command can be used to select accounts of interest (@pxref{Selection -Expressions,,,recutils, GNU recutils manual}). Note that period -characters (@samp{.}) found in the account parameter keys are mapped to -underscores (@samp{_}) in the output, to meet the requirements of the -Recutils format. The following example shows how to print the account -fingerprints for all accounts operating in the rendezvous point mode: - -@example sh -# herd list-account-details jami | \ - recsel -p Account.username -e 'Account.rendezVous ~ "true"' -Account_username: f3345f2775ddfe07a4b0d95daea111d15fbc1199 -@end example - -The remaining actions should be self-explanatory. - -The complete set of available configuration options is detailed below. - -@c TODO: Ideally, the following fragments would be auto-generated at -@c build time, so that they needn't be manually duplicated. -@c Auto-generated via (configuration->documentation 'jami-configuration) -@deftp {Data Type} jami-configuration -Available @code{jami-configuration} fields are: - -@table @asis -@item @code{jamid} (default: @code{libring}) (type: package) -The Jami daemon package to use. - -@item @code{dbus} (default: @code{dbus}) (type: package) -The D-Bus package to use to start the required D-Bus session. - -@item @code{nss-certs} (default: @code{nss-certs}) (type: package) -The nss-certs package to use to provide TLS certificates. - -@item @code{enable-logging?} (default: @code{#t}) (type: boolean) -Whether to enable logging to syslog. - -@item @code{debug?} (default: @code{#f}) (type: boolean) -Whether to enable debug level messages. - -@item @code{auto-answer?} (default: @code{#f}) (type: boolean) -Whether to force automatic answer to incoming calls. - -@item @code{accounts} (default: @code{disabled}) (type: maybe-jami-account-list) -A list of Jami accounts to be (re-)provisioned every time the Jami -daemon service starts. When providing this field, the account -directories under @file{/var/lib/jami/} are recreated every time the -service starts, ensuring a consistent state. - -@end table - -@end deftp - -@c Auto-generated via (configuration->documentation 'jami-account) -@deftp {Data Type} jami-account -Available @code{jami-account} fields are: - -@table @asis -@item @code{archive} (type: string-or-computed-file) -The account archive (backup) file name of the account. This is used to -provision the account when the service starts. The account archive -should @emph{not} be encrypted. It is highly recommended to make it -readable only to the @samp{root} user (i.e., not in the store), to guard -against leaking the secret key material of the Jami account it contains. - -@item @code{allowed-contacts} (default: @code{disabled}) (type: maybe-account-fingerprint-list) -The list of allowed contacts for the account, entered as their 40 -characters long fingerprint. Messages or calls from accounts not in -that list will be rejected. When unspecified, the configuration of the -account archive is used as-is with respect to contacts and public -inbound calls/messaging allowance, which typically defaults to allow any -contact to communicate with the account. - -@item @code{moderators} (default: @code{disabled}) (type: maybe-account-fingerprint-list) -The list of contacts that should have moderation privileges (to ban, -mute, etc. other users) in rendezvous conferences, entered as their 40 -characters long fingerprint. When unspecified, the configuration of the -account archive is used as-is with respect to moderation, which -typically defaults to allow anyone to moderate. - -@item @code{rendezvous-point?} (default: @code{disabled}) (type: maybe-boolean) -Whether the account should operate in the rendezvous mode. In this -mode, all the incoming audio/video calls are mixed into a conference. -When left unspecified, the value from the account archive prevails. - -@item @code{peer-discovery?} (default: @code{disabled}) (type: maybe-boolean) -Whether peer discovery should be enabled. Peer discovery is used to -discover other OpenDHT nodes on the local network, which can be useful -to maintain communication between devices on such network even when the -connection to the the Internet has been lost. When left unspecified, -the value from the account archive prevails. - -@item @code{bootstrap-hostnames} (default: @code{disabled}) (type: maybe-string-list) -A list of hostnames or IPs pointing to OpenDHT nodes, that should be -used to initially join the OpenDHT network. When left unspecified, the -value from the account archive prevails. - -@item @code{name-server-uri} (default: @code{disabled}) (type: maybe-string) -The URI of the name server to use, that can be used to retrieve the -account fingerprint for a registered username. - -@end table - -@end deftp - -@subsubheading Murmur (VoIP server) - @cindex Murmur (VoIP server) @cindex VoIP server This section describes how to set up and run a Murmur server. Murmur is diff --git a/gnu/build/jami-service.scm b/gnu/build/jami-service.scm deleted file mode 100644 index d44e87387d..0000000000 --- a/gnu/build/jami-service.scm +++ /dev/null @@ -1,587 +0,0 @@ -;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2021 Maxim Cournoyer -;;; -;;; This file is part of GNU Guix. -;;; -;;; GNU Guix is free software; you can redistribute it and/or modify it -;;; under the terms of the GNU General Public License as published by -;;; the Free Software Foundation; either version 3 of the License, or (at -;;; your option) any later version. -;;; -;;; GNU Guix 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 General Public License for more details. -;;; -;;; You should have received a copy of the GNU General Public License -;;; along with GNU Guix. If not, see . - -;;; Commentary: -;;; -;;; This module contains helpers used as part of the jami-service-type -;;; definition. -;;; -;;; Code: - -(define-module (gnu build jami-service) - #:use-module (ice-9 format) - #:use-module (ice-9 match) - #:use-module (ice-9 peg) - #:use-module (ice-9 rdelim) - #:use-module (ice-9 regex) - #:use-module (rnrs io ports) - #:autoload (shepherd service) (fork+exec-command) - #:use-module (srfi srfi-1) - #:use-module (srfi srfi-26) - #:export (account-fingerprint? - account-details->recutil - get-accounts - get-usernames - set-account-details - add-account - account->username - username->account - username->contacts - enable-account - disable-account - - add-contact - remove-contact - - set-all-moderators - set-moderator - username->all-moderators? - username->moderators - - dbus-available-services - dbus-service-available? - - %send-dbus-binary - %send-dbus-bus - %send-dbus-user - %send-dbus-group - %send-dbus-debug - send-dbus - - with-retries)) - -;;; -;;; Utilities. -;;; - -(define-syntax-rule (with-retries n delay body ...) - "Retry the code in BODY up to N times until it doesn't raise an exception -nor return #f, else raise an error. A delay of DELAY seconds is inserted -before each retry." - (let loop ((attempts 0)) - (catch #t - (lambda () - (let ((result (begin body ...))) - (if (not result) - (error "failed attempt" attempts) - result))) - (lambda args - (if (< attempts n) - (begin - (sleep delay) ;else wait and retry - (loop (+ 1 attempts))) - (error "maximum number of retry attempts reached" - body ... args)))))) - -(define (alist->list alist) - "Flatten ALIST into a list." - (append-map (match-lambda - (() '()) - ((key . value) - (list key value))) - alist)) - -(define account-fingerprint-rx (make-regexp "[0-9A-f]{40}")) - -(define (account-fingerprint? val) - "A Jami account fingerprint is 40 characters long and only contains -hexadecimal characters." - (and (string? val) - (regexp-exec account-fingerprint-rx val))) - - -;;; -;;; D-Bus reply parser. -;;; - -(define (parse-dbus-reply reply) - "Return the parse tree of REPLY, a string returned by the 'dbus-send' -command." - ;; Refer to 'man 1 dbus-send' for the grammar reference. Note that the - ;; format of the replies doesn't match the format of the input, which is the - ;; one documented, but it gives an idea. For an even better reference, see - ;; the `print_iter' procedure of the 'dbus-print-message.c' file from the - ;; 'dbus' package sources. - (define-peg-string-patterns - "contents <- header (item / container (item / container*)?) - item <-- WS type WS value NL - container <- array / dict / variant - array <-- array-start (item / container)* array-end - dict <-- array-start dict-entry* array-end - dict-entry <-- dict-entry-start item item dict-entry-end - variant <-- variant-start item - type <-- 'string' / 'int16' / 'uint16' / 'int32' / 'uint32' / 'int64' / - 'uint64' / 'double' / 'byte' / 'boolean' / 'objpath' - value <-- (!NL .)* NL - header < (!NL .)* NL - variant-start < WS 'variant' - array-start < WS 'array [' NL - array-end < WS ']' NL - dict-entry-start < WS 'dict entry(' NL - dict-entry-end < WS ')' NL - DQ < '\"' - WS < ' '* - NL < '\n'*") - - (peg:tree (match-pattern contents reply))) - -(define (strip-quotes text) - "Strip the leading and trailing double quotes (\") characters from TEXT." - (let* ((text* (if (string-prefix? "\"" text) - (string-drop text 1) - text)) - (text** (if (string-suffix? "\"" text*) - (string-drop-right text* 1) - text*))) - text**)) - -(define (deserialize-item item) - "Return the value described by the ITEM parse tree as a Guile object." - ;; Strings are printed wrapped in double quotes (see the print_iter - ;; procedure in dbus-print-message.c). - (match item - (('item ('type "string") ('value value)) - (strip-quotes value)) - (('item ('type "boolean") ('value value)) - (if (string=? "true" value) - #t - #f)) - (('item _ ('value value)) - value))) - -(define (serialize-boolean bool) - "Return the serialized format expected by dbus-send for BOOL." - (format #f "boolean:~:[false~;true~]" bool)) - -(define (dict->alist dict-parse-tree) - "Translate a dict parse tree to an alist." - (define (tuples->alist tuples) - (map (lambda (x) (apply cons x)) tuples)) - - (match dict-parse-tree - ('dict - '()) - (('dict ('dict-entry keys values) ...) - (let ((keys* (map deserialize-item keys)) - (values* (map deserialize-item values))) - (tuples->alist (zip keys* values*)))))) - -(define (array->list array-parse-tree) - "Translate an array parse tree to a list." - (match array-parse-tree - ('array - '()) - (('array items ...) - (map deserialize-item items)))) - - -;;; -;;; Low-level, D-Bus-related procedures. -;;; - -;;; The following parameters are used in the jami-service-type service -;;; definition to conveniently customize the behavior of the send-dbus helper, -;;; even when called indirectly. -(define %send-dbus-binary (make-parameter "dbus-send")) -(define %send-dbus-bus (make-parameter #f)) -(define %send-dbus-user (make-parameter #f)) -(define %send-dbus-group (make-parameter #f)) -(define %send-dbus-debug (make-parameter #f)) - -(define* (send-dbus #:key service path interface method - bus - dbus-send - user group - timeout - arguments) - "Return the response of DBUS-SEND, else raise an error. Unless explicitly -provided, DBUS-SEND takes the value of the %SEND-DBUS-BINARY parameter. BUS -can be used to specify the bus address, such as 'unix:path=/var/run/jami/bus'. -Alternatively, the %SEND-DBUS-BUS parameter can be used. ARGUMENTS can be -used to pass input values to a D-Bus method call. TIMEOUT is the amount of -time to wait for a reply in milliseconds before giving up with an error. USER -and GROUP allow choosing under which user/group the DBUS-SEND command is -executed. Alternatively, the %SEND-DBUS-USER and %SEND-DBUS-GROUP parameters -can be used instead." - (let* ((command `(,(if dbus-send - dbus-send - (%send-dbus-binary)) - ,@(if (or bus (%send-dbus-bus)) - (list (string-append "--bus=" - (or bus (%send-dbus-bus)))) - '()) - "--print-reply" - ,@(if timeout - (list (format #f "--reply-timeout=~d" timeout)) - '()) - ,(string-append "--dest=" service) ;e.g., cx.ring.Ring - ,path ;e.g., /cx/ring/Ring/ConfigurationManager - ,(string-append interface "." method) - ,@(or arguments '()))) - (temp-port (mkstemp! (string-copy "/tmp/dbus-send-output-XXXXXXX"))) - (temp-file (port-filename temp-port))) - (dynamic-wind - (lambda () - (let* ((uid (or (and=> (or user (%send-dbus-user)) - (compose passwd:uid getpwnam)) -1)) - (gid (or (and=> (or group (%send-dbus-group)) - (compose group:gid getgrnam)) -1))) - (chown temp-port uid gid))) - (lambda () - (let ((pid (fork+exec-command command - #:user (or user (%send-dbus-user)) - #:group (or group (%send-dbus-group)) - #:log-file temp-file))) - (match (waitpid pid) - ((_ . status) - (let ((exit-status (status:exit-val status)) - (output (call-with-port temp-port get-string-all))) - (if (= 0 exit-status) - output - (error "the send-dbus command exited with: " - command exit-status output))))))) - (lambda () - (false-if-exception (delete-file temp-file)))))) - -(define (parse-account-ids reply) - "Return the Jami account IDs from REPLY, which is assumed to be the output -of the Jami D-Bus `getAccountList' method." - (array->list (parse-dbus-reply reply))) - -(define (parse-account-details reply) - "Parse REPLY, which is assumed to be the output of the Jami D-Bus -`getAccountDetails' method, and return its content as an alist." - (dict->alist (parse-dbus-reply reply))) - -(define (parse-contacts reply) - "Parse REPLY, which is assumed to be the output of the Jamid D-Bus -`getContacts' method, and return its content as an alist." - (match (parse-dbus-reply reply) - ('array - '()) - (('array dicts ...) - (map dict->alist dicts)))) - - -;;; -;;; Higher-level, D-Bus-related procedures. -;;; - -(define (validate-fingerprint fingerprint) - "Validate that fingerprint is 40 characters long." - (unless (account-fingerprint? fingerprint) - (error "Account fingerprint is not valid:" fingerprint))) - -(define (dbus-available-services) - "Return the list of available (acquired) D-Bus services." - (let ((reply (parse-dbus-reply - (send-dbus #:service "org.freedesktop.DBus" - #:path "/org/freedesktop/DBus" - #:interface "org.freedesktop.DBus" - #:method "ListNames")))) - ;; Remove entries such as ":1.7". - (remove (cut string-prefix? ":" <>) - (array->list reply)))) - -(define (dbus-service-available? service) - "Predicate to check for the D-Bus SERVICE availability." - (member service (dbus-available-services))) - -(define* (send-dbus/configuration-manager #:key method arguments timeout) - "Query the Jami D-Bus ConfigurationManager service." - (send-dbus #:service "cx.ring.Ring" - #:path "/cx/ring/Ring/ConfigurationManager" - #:interface "cx.ring.Ring.ConfigurationManager" - #:method method - #:arguments arguments - #:timeout timeout)) - -;;; The following methods are for internal use; they make use of the account -;;; ID, an implementation detail of Jami the user should not need to be -;;; concerned with. -(define (get-account-ids) - "Return the available Jami account identifiers (IDs). Account IDs are an -implementation detail used to identify the accounts in Jami." - (parse-account-ids - (send-dbus/configuration-manager #:method "getAccountList"))) - -(define (id->account-details id) - "Retrieve the account data associated with the given account ID." - (parse-account-details - (send-dbus/configuration-manager - #:method "getAccountDetails" - #:arguments (list (string-append "string:" id))))) - -(define (id->volatile-account-details id) - "Retrieve the account data associated with the given account ID." - (parse-account-details - (send-dbus/configuration-manager - #:method "getVolatileAccountDetails" - #:arguments (list (string-append "string:" id))))) - -(define (id->account id) - "Retrieve the complete account data associated with the given account ID." - (append (id->volatile-account-details id) - (id->account-details id))) - -(define %username-to-id-cache #f) - -(define (invalidate-username-to-id-cache!) - (set! %username-to-id-cache #f)) - -(define (username->id username) - "Return the first account ID corresponding to USERNAME." - (unless (assoc-ref %username-to-id-cache username) - (set! %username-to-id-cache - (append-map - (lambda (id) - (let* ((account (id->account id)) - (username (assoc-ref account "Account.username")) - (registered-name (assoc-ref account - "Account.registeredName"))) - `(,@(if username - (list (cons username id)) - '()) - ,@(if registered-name - (list (cons registered-name id)) - '())))) - (get-account-ids)))) - (or (assoc-ref %username-to-id-cache username) - (let ((message (format #f "Could not retrieve a local account ID\ - for ~:[username~;fingerprint~]" (account-fingerprint? username)))) - (error message username)))) - -(define (account->username account) - "Return USERNAME, the registered username associated with ACCOUNT, else its -public key fingerprint." - (or (assoc-ref account "Account.registeredName") - (assoc-ref account "Account.username"))) - -(define (id->username id) - "Return USERNAME, the registered username associated with ID, else its -public key fingerprint, else #f." - (account->username (id->account id))) - -(define (get-accounts) - "Return the list of all accounts, as a list of alists." - (map id->account (get-account-ids))) - -(define (get-usernames) - "Return the list of the usernames associated with the present accounts." - (map account->username (get-accounts))) - -(define (username->account username) - "Return the first account associated with USERNAME, else #f. -USERNAME can be either the account 40 characters public key fingerprint or a -registered username." - (find (lambda (account) - (member username - (list (assoc-ref account "Account.username") - (assoc-ref account "Account.registeredName")))) - (get-accounts))) - -(define (add-account archive) - "Import the Jami account ARCHIVE and return its account ID. The archive -should *not* be encrypted with a password. Return the username associated -with the account." - (invalidate-username-to-id-cache!) - (let ((reply (send-dbus/configuration-manager - #:method "addAccount" - #:arguments (list (string-append - "dict:string:string:Account.archivePath," - archive - ",Account.type,RING"))))) - ;; The account information takes some time to be populated. - (let ((id (deserialize-item (parse-dbus-reply reply)))) - (with-retries 20 1 - (let ((username (id->username id))) - (if (string-null? username) - #f - username)))))) - -(define (remove-account username) - "Delete the Jami account associated with USERNAME, the account 40 characters -fingerprint or a registered username." - (let ((id (username->id username))) - (send-dbus/configuration-manager - #:method "removeAccount" - #:arguments (list (string-append "string:" id)))) - (invalidate-username-to-id-cache!)) - -(define* (username->contacts username) - "Return the contacts associated with the account of USERNAME as two values; -the first one being the regular contacts and the second one the banned -contacts. USERNAME can be either the account 40 characters public key -fingerprint or a registered username. The contacts returned are represented -using their 40 characters fingerprint." - (let* ((id (username->id username)) - (reply (send-dbus/configuration-manager - #:method "getContacts" - #:arguments (list (string-append "string:" id)))) - (all-contacts (parse-contacts reply)) - (banned? (lambda (contact) - (and=> (assoc-ref contact "banned") - (cut string=? "true" <>)))) - (banned (filter banned? all-contacts)) - (not-banned (filter (negate banned?) all-contacts)) - (fingerprint (cut assoc-ref <> "id"))) - (values (map fingerprint not-banned) - (map fingerprint banned)))) - -(define* (remove-contact contact username #:key ban?) - "Remove CONTACT, the 40 characters public key fingerprint of a contact, from -the account associated with USERNAME (either a fingerprint or a registered -username). When BAN? is true, also mark the contact as banned." - (validate-fingerprint contact) - (let ((id (username->id username))) - (send-dbus/configuration-manager - #:method "removeContact" - #:arguments (list (string-append "string:" id) - (string-append "string:" contact) - (serialize-boolean ban?))))) - -(define (add-contact contact username) - "Add CONTACT, the 40 characters public key fingerprint of a contact, to the -account of USERNAME (either a fingerprint or a registered username)." - (validate-fingerprint contact) - (let ((id (username->id username))) - (send-dbus/configuration-manager - #:method "addContact" - #:arguments (list (string-append "string:" id) - (string-append "string:" contact))))) - -(define* (set-account-details details username #:key timeout) - "Set DETAILS, an alist containing the key value pairs to set for the account -of USERNAME, a registered username or account fingerprint. The value of the -parameters not provided are unchanged. TIMEOUT is a value in milliseconds to -pass to the `send-dbus/configuration-manager' procedure." - (let* ((id (username->id username)) - (current-details (id->account-details id)) - (updated-details (map (match-lambda - ((key . value) - (or (and=> (assoc-ref details key) - (cut cons key <>)) - (cons key value)))) - current-details)) - ;; dbus-send does not permit sending null strings (it throws a - ;; "malformed dictionary" error). Luckily they seem to have the - ;; semantic of "default account value" in Jami; so simply drop them. - (updated-details* (remove (match-lambda - ((_ . value) - (string-null? value))) - updated-details))) - (send-dbus/configuration-manager - #:timeout timeout - #:method "setAccountDetails" - #:arguments - (list (string-append "string:" id) - (string-append "dict:string:string:" - (string-join (alist->list updated-details*) - ",")))))) - -(define (set-all-moderators enabled? username) - "Set the 'AllModerators' property to enabled? for the account of USERNAME, a -registered username or account fingerprint." - (let ((id (username->id username))) - (send-dbus/configuration-manager - #:method "setAllModerators" - #:arguments - (list (string-append "string:" id) - (serialize-boolean enabled?))))) - -(define (username->all-moderators? username) - "Return the 'AllModerators' property for the account of USERNAME, a -registered username or account fingerprint." - (let* ((id (username->id username)) - (reply (send-dbus/configuration-manager - #:method "isAllModerators" - #:arguments - (list (string-append "string:" id))))) - (deserialize-item (parse-dbus-reply reply)))) - -(define (username->moderators username) - "Return the moderators for the account of USERNAME, a registered username or -account fingerprint." - (let* ((id (username->id username)) - (reply (send-dbus/configuration-manager - #:method "getDefaultModerators" - #:arguments - (list (string-append "string:" id))))) - (array->list (parse-dbus-reply reply)))) - -(define (set-moderator contact enabled? username) - "Set the moderator flag to ENABLED? for CONTACT, the 40 characters public -key fingerprint of a contact for the account of USERNAME, a registered -username or account fingerprint." - (validate-fingerprint contact) - (let* ((id (username->id username))) - (send-dbus/configuration-manager #:method "setDefaultModerator" - #:arguments - (list (string-append "string:" id) - (string-append "string:" contact) - (serialize-boolean enabled?))))) - -(define (disable-account username) - "Disable the account known by USERNAME, a registered username or account -fingerprint." - (set-account-details '(("Account.enable" . "false")) username - ;; Waiting for the reply on this command takes a very - ;; long time that trips the default D-Bus timeout value - ;; (25 s), for some reason. - #:timeout 60000)) - -(define (enable-account username) - "Enable the account known by USERNAME, a registered username or account -fingerprint." - (set-account-details '(("Account.enable" . "true")) username)) - - -;;; -;;; Presentation procedures. -;;; - -(define (.->_ text) - "Map each period character to underscore characters." - (string-map (match-lambda - (#\. #\_) - (c c)) - text)) - -(define (account-details->recutil account-details) - "Serialize the account-details alist into a recutil string. Period -characters in the keys are normalized to underscore to meet Recutils' format -requirements." - (define (pair->recutil-property pair) - (match pair - ((key . value) - (string-append (.->_ key) ": " value)))) - - (define sorted-account-details - ;; Have the account username, display name and alias appear first, for - ;; convenience. - (let ((first-items '("Account.username" - "Account.displayName" - "Account.alias"))) - (append (map (cut assoc <> account-details) first-items) - (fold alist-delete account-details first-items)))) - - (string-join (map pair->recutil-property sorted-account-details) "\n")) - -;; Local Variables: -;; eval: (put 'with-retries 'scheme-indent-function 2) -;; End: diff --git a/gnu/local.mk b/gnu/local.mk index 66a2ba52bc..9e8f2d702d 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -685,7 +685,6 @@ GNU_SYSTEM_MODULES = \ %D%/build/chromium-extension.scm \ %D%/build/cross-toolchain.scm \ %D%/build/image.scm \ - %D%/build/jami-service.scm \ %D%/build/file-systems.scm \ %D%/build/hurd-boot.scm \ %D%/build/install.scm \ @@ -723,7 +722,6 @@ GNU_SYSTEM_MODULES = \ %D%/tests/security-token.scm \ %D%/tests/singularity.scm \ %D%/tests/ssh.scm \ - %D%/tests/telephony.scm \ %D%/tests/version-control.scm \ %D%/tests/virtualization.scm \ %D%/tests/web.scm @@ -1883,8 +1881,7 @@ dist_patch_DATA = \ %D%/packages/patches/ytnef-CVE-2021-3403.patch \ %D%/packages/patches/ytnef-CVE-2021-3404.patch \ %D%/packages/patches/zstd-CVE-2021-24031_CVE-2021-24032.patch \ - %D%/packages/patches/zziplib-CVE-2018-16548.patch \ - %D%/tests/data/jami-dummy-account.dat + %D%/packages/patches/zziplib-CVE-2018-16548.patch MISC_DISTRO_FILES = \ %D%/packages/ld-wrapper.in diff --git a/gnu/services/telephony.scm b/gnu/services/telephony.scm index fd90840324..e1259cc2df 100644 --- a/gnu/services/telephony.scm +++ b/gnu/services/telephony.scm @@ -1,6 +1,5 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2017 nee -;;; Copyright © 2021 Maxim Cournoyer ;;; ;;; This file is part of GNU Guix. ;;; @@ -18,45 +17,16 @@ ;;; along with GNU Guix. If not, see . (define-module (gnu services telephony) - #:use-module ((gnu build jami-service) #:select (account-fingerprint?)) - #:use-module ((gnu services) #:hide (delete)) - #:use-module (gnu services configuration) + #:use-module (gnu services) #:use-module (gnu services shepherd) #:use-module (gnu system shadow) #:use-module (gnu packages admin) - #:use-module (gnu packages certs) - #:use-module (gnu packages glib) - #:use-module (gnu packages jami) #:use-module (gnu packages telephony) #:use-module (guix records) - #:use-module (guix modules) - #:use-module (guix packages) #:use-module (guix gexp) #:use-module (srfi srfi-1) - #:use-module (srfi srfi-2) - #:use-module (srfi srfi-26) - #:use-module (ice-9 format) #:use-module (ice-9 match) - #:export (jami-account - jami-account-archive - jami-account-allowed-contacts - jami-account-moderators - jami-account-rendezvous-point? - jami-account-discovery? - jami-account-bootstrap-uri - jami-account-name-server-uri - - jami-configuration - jami-configuration-jamid - jami-configuration-dbus - jami-configuration-enable-logging? - jami-configuration-debug? - jami-configuration-auto-answer? - jami-configuration-accounts - - jami-service-type - - murmur-configuration + #:export (murmur-configuration make-murmur-configuration murmur-configuration? murmur-configuration-package @@ -104,652 +74,6 @@ (define-module (gnu services telephony) murmur-service-type)) - -;;; -;;; Jami daemon. -;;; - -;;; XXX: Passing a computed-file object as the account is used for tests. -(define (string-or-computed-file? val) - (or (string? val) - (computed-file? val))) - -(define (string-list? val) - (and (list? val) - (and-map string? val))) - -(define (account-fingerprint-list? val) - (and (list? val) - (and-map account-fingerprint? val))) - -(define-maybe string-list) - -(define-maybe/no-serialization account-fingerprint-list) - -(define-maybe boolean) - -(define-maybe string) - -;;; The following serializers are used to derive an account details alist from -;;; a record. -(define (serialize-string-list _ val) - (string-join val ";")) - -(define (serialize-boolean _ val) - (format #f "~:[false~;true~]" val)) - -(define (serialize-string _ val) - val) - -;;; Note: Serialization is used to produce an account details alist that can -;;; be passed to the SET-ACCOUNT-DETAILS procedure. Fields that do not map to -;;; a Jami account 'detail' should have their serialization disabled via the -;;; 'empty-serializer' procedure. -(define-configuration jami-account - (archive - (string-or-computed-file) - "The account archive (backup) file name of the account. This is used to -provision the account when the service starts. The account archive should -@emph{not} be encrypted. It is highly recommended to make it readable only to -the @samp{root} user (i.e., not in the store), to guard against leaking the -secret key material of the Jami account it contains." - empty-serializer) - (allowed-contacts - (maybe-account-fingerprint-list 'disabled) - "The list of allowed contacts for the account, entered as their 40 -characters long fingerprint. Messages or calls from accounts not in that list -will be rejected. When unspecified, the configuration of the account archive -is used as-is with respect to contacts and public inbound calls/messaging -allowance, which typically defaults to allow any contact to communicate with -the account." - empty-serializer) - (moderators - (maybe-account-fingerprint-list 'disabled) - "The list of contacts that should have moderation privileges (to ban, mute, -etc. other users) in rendezvous conferences, entered as their 40 characters -long fingerprint. When unspecified, the configuration of the account archive -is used as-is with respect to moderation, which typically defaults to allow -anyone to moderate." - empty-serializer) - ;; The serializable fields below are to be set with set-account-details. - (rendezvous-point? - (maybe-boolean 'disabled) - "Whether the account should operate in the rendezvous mode. In this mode, -all the incoming audio/video calls are mixed into a conference. When left -unspecified, the value from the account archive prevails.") - (peer-discovery? - (maybe-boolean 'disabled) - "Whether peer discovery should be enabled. Peer discovery is used to -discover other OpenDHT nodes on the local network, which can be useful to -maintain communication between devices on such network even when the -connection to the the Internet has been lost. When left unspecified, the -value from the account archive prevails.") - (bootstrap-hostnames - (maybe-string-list 'disabled) - "A list of hostnames or IPs pointing to OpenDHT nodes, that should be used -to initially join the OpenDHT network. When left unspecified, the value from -the account archive prevails.") - (name-server-uri - (maybe-string 'disabled) - "The URI of the name server to use, that can be used to retrieve the -account fingerprint for a registered username.")) - -(define (jami-account->alist jami-account-object) - "Serialize the JAMI-ACCOUNT object as an alist suitable to be passed to -SET-ACCOUNT-DETAILS." - (define (field-name->account-detail name) - (match name - ('rendezvous-point? "Account.rendezVous") - ('peer-discovery? "Account.peerDiscovery") - ('bootstrap-hostnames "Account.hostname") - ('name-server-uri "RingNS.uri") - (_ #f))) - - (filter-map (lambda (field) - (and-let* ((name (field-name->account-detail - (configuration-field-name field))) - (value ((configuration-field-serializer field) - name ((configuration-field-getter field) - jami-account-object))) - ;; The define-maybe default serializer produces an - ;; empty string for the 'disabled value. - (value* (if (string-null? value) - #f - value))) - (cons name value*))) - jami-account-fields)) - -(define (jami-account-list? val) - (and (list? val) - (and-map jami-account? val))) - -(define-maybe/no-serialization jami-account-list) - -(define-configuration/no-serialization jami-configuration - (jamid - (package libring) - "The Jami daemon package to use.") - (dbus - (package dbus) - "The D-Bus package to use to start the required D-Bus session.") - (nss-certs - (package nss-certs) - "The nss-certs package to use to provide TLS certificates.") - (enable-logging? - (boolean #t) - "Whether to enable logging to syslog.") - (debug? - (boolean #f) - "Whether to enable debug level messages.") - (auto-answer? - (boolean #f) - "Whether to force automatic answer to incoming calls.") - (accounts - (maybe-jami-account-list 'disabled) - "A list of Jami accounts to be (re-)provisioned every time the Jami daemon -service starts. When providing this field, the account directories under -@file{/var/lib/jami/} are recreated every time the service starts, ensuring a -consistent state.")) - -(define %jami-accounts - (list (user-group (name "jami") (system? #t)) - (user-account - (name "jami") - (group "jami") - (system? #t) - (comment "Jami daemon user") - (home-directory "/var/lib/jami")))) - -(define (jami-configuration->command-line-arguments config) - "Derive the command line arguments to used to launch the Jami daemon from -CONFIG, a object." - (match-record config - (jamid dbus enable-logging? debug? auto-answer?) - `(,(file-append jamid "/lib/ring/dring") - "--persistent" ;stay alive after client quits - ,@(if enable-logging? - '() ;logs go to syslog by default - (list "--console")) ;else stdout/stderr - ,@(if debug? - (list "--debug") - '()) - ,@(if auto-answer? - (list "--auto-answer") - '())))) - -(define (jami-dbus-session-activation config) - "Create a directory to hold the Jami D-Bus session socket." - (with-imported-modules (source-module-closure '((gnu build activation))) - #~(begin - (use-modules (gnu build activation)) - (let ((user (getpwnam "jami"))) - (mkdir-p/perms "/var/run/jami" user #o700))))) - -(define (jami-shepherd-services config) - "Return a running the Jami daemon." - (let* ((jamid (jami-configuration-jamid config)) - (nss-certs (jami-configuration-nss-certs config)) - (dbus (jami-configuration-dbus config)) - (dbus-daemon (file-append dbus "/bin/dbus-daemon")) - (dbus-send (file-append dbus "/bin/dbus-send")) - (accounts (jami-configuration-accounts config)) - (declarative-mode? (not (eq? 'disabled accounts)))) - - (with-imported-modules (source-module-closure - '((gnu build jami-service) - (gnu build shepherd) - (gnu system file-systems))) - - (define list-accounts-action - (shepherd-action - (name 'list-accounts) - (documentation "List the available Jami accounts. Return the account -details alists keyed by their account username.") - (procedure - #~(lambda _ - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - ;; Print the accounts summary or long listing, according to - ;; user-provided option. - (let* ((usernames (get-usernames)) - (accounts (map-in-order username->account usernames))) - (match accounts - (() ;empty list - (format #t "There is no Jami account available.~%")) - ((one two ...) - (format #t "The following Jami accounts are available:~%") - (for-each - (lambda (account) - (define fingerprint (assoc-ref account - "Account.username")) - (define human-friendly-name - (or (assoc-ref account - "Account.registeredName") - (assoc-ref account - "Account.displayName") - (assoc-ref account - "Account.alias"))) - (define disabled? - (and=> (assoc-ref account "Account.enable") - (cut string=? "false" <>))) - - (format #t " - ~a~@[ (~a)~] ~:[~;[disabled]~]~%" - fingerprint human-friendly-name disabled?)) - accounts) - (display "\n"))) - ;; Return the account-details-list alist. - (map cons usernames accounts))))))) - - (define list-account-details-action - (shepherd-action - (name 'list-account-details) - (documentation "Display the account details of the available Jami -accounts in the @code{recutils} format. Return the account details alists -keyed by their account username.") - (procedure - #~(lambda _ - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (let* ((usernames (get-usernames)) - (accounts (map-in-order username->account usernames))) - (for-each (lambda (account) - (display (account-details->recutil account)) - (display "\n\n")) - accounts) - (map cons usernames accounts))))))) - - (define list-contacts-action - (shepherd-action - (name 'list-contacts) - (documentation "Display the contacts for each Jami account. Return -an alist containing the contacts keyed by the account usernames.") - (procedure - #~(lambda _ - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (let* ((usernames (get-usernames)) - (contacts (map-in-order username->contacts usernames))) - (for-each (lambda (username contacts) - (format #t "Contacts for account ~a:~%" - username) - (format #t "~{ - ~a~%~}~%" contacts)) - usernames contacts) - (map cons usernames contacts))))))) - - (define list-moderators-action - (shepherd-action - (name 'list-moderators) - (documentation "Display the moderators for each Jami account. Return -an alist containing the moderators keyed by the account usernames.") - (procedure - #~(lambda _ - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (let* ((usernames (get-usernames)) - (moderators (map-in-order username->moderators - usernames))) - (for-each - (lambda (username moderators) - (if (username->all-moderators? username) - (format #t "Anyone can moderate for account ~a~%" - username) - (begin - (format #t "Moderators for account ~a:~%" username) - (format #t "~{ - ~a~%~}~%" moderators)))) - usernames moderators) - (map cons usernames moderators))))))) - - (define add-moderator-action - (shepherd-action - (name 'add-moderator) - (documentation "Add a moderator for a given Jami account. The -MODERATOR contact must be given as its 40 characters fingerprint, while the -Jami account can be provided as its registered USERNAME or fingerprint. - -@example -herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f username -@end example - -Return the moderators for the account known by USERNAME.") - (procedure - #~(lambda (_ moderator username) - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (set-all-moderators #f username) - (add-contact moderator username) - (set-moderator moderator #t username) - (username->moderators username)))))) - - (define ban-contact-action - (shepherd-action - (name 'ban-contact) - (documentation "Ban a contact for a given or all Jami accounts, and -clear their moderator flag. The CONTACT must be given as its 40 characters -fingerprint, while the Jami account can be provided as its registered USERNAME -or fingerprint, or omitted. When the account is omitted, CONTACT is banned -from all accounts. - -@example -herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f [username] -@end example") - (procedure - #~(lambda* (_ contact #:optional username) - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (let ((usernames (or (and=> username list) - (get-usernames)))) - (for-each (lambda (username) - (set-moderator contact #f username) - (remove-contact contact username #:ban? #t)) - usernames))))))) - - (define list-banned-contacts-action - (shepherd-action - (name 'list-banned-contacts) - (documentation "List the banned contacts for each accounts. Return -an alist of the banned contacts, keyed by the account usernames.") - (procedure - #~(lambda _ - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - - (define banned-contacts - (let ((usernames (get-usernames))) - (map cons usernames - (map-in-order (lambda (x) - (receive (_ banned) - (username->contacts x) - banned)) - usernames)))) - - (for-each (match-lambda - ((username . banned) - (unless (null? banned) - (format #t "Banned contacts for account ~a:~%" - username) - (format #t "~{ - ~a~%~}~%" banned)))) - banned-contacts) - banned-contacts))))) - - (define enable-account-action - (shepherd-action - (name 'enable-account) - (documentation "Enable an account. It takes USERNAME as an argument, -either a registered username or the fingerprint of the account.") - (procedure - #~(lambda (_ username) - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (enable-account username)))))) - - (define disable-account-action - (shepherd-action - (name 'disable-account) - (documentation "Disable an account. It takes USERNAME as an -argument, either a registered username or the fingerprint of the account.") - (procedure - #~(lambda (_ username) - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - (disable-account username)))))) - - (list (shepherd-service - (documentation "Run a D-Bus session for the Jami daemon.") - (provision '(jami-dbus-session)) - (modules `((gnu build shepherd) - (gnu build jami-service) - (gnu system file-systems) - ,@%default-modules)) - ;; The requirement on dbus-system is to ensure other required - ;; activation for D-Bus, such as a /etc/machine-id file. - (requirement '(dbus-system syslogd)) - (start - #~(lambda args - (define pid - ((make-forkexec-constructor/container - (list #$dbus-daemon "--session" - "--address=unix:path=/var/run/jami/bus" - "--nofork" "--syslog-only" "--nopidfile") - #:mappings (list (file-system-mapping - (source "/dev/log") ;for syslog - (target source)) - (file-system-mapping - (source "/var/run/jami") - (target source) - (writable? #t))) - #:user "jami" - #:group "jami" - #:environment-variables - ;; This is so that the cx.ring.Ring service D-Bus - ;; definition is found by dbus-send. - (list (string-append "XDG_DATA_DIRS=" - #$jamid "/share"))))) - - ;; XXX: This manual synchronization probably wouldn't be - ;; needed if we were using a PID file, but providing it via a - ;; customized config file with would not override - ;; the one inherited from the base config of D-Bus. - (let ((sock (socket PF_UNIX SOCK_STREAM 0))) - (with-retries 20 1 (catch 'system-error - (lambda () - (connect sock AF_UNIX - "/var/run/jami/bus") - (close-port sock) - #t) - (lambda args - #f)))) - - pid)) - (stop #~(make-kill-destructor))) - - (shepherd-service - (documentation "Run the Jami daemon.") - (provision '(jami)) - (actions (list list-accounts-action - list-account-details-action - list-contacts-action - list-moderators-action - add-moderator-action - ban-contact-action - list-banned-contacts-action - enable-account-action - disable-account-action)) - (requirement '(jami-dbus-session)) - (modules `((ice-9 format) - (ice-9 ftw) - (ice-9 match) - (ice-9 receive) - (srfi srfi-1) - (srfi srfi-26) - (gnu build jami-service) - (gnu build shepherd) - (gnu system file-systems) - ,@%default-modules)) - (start - #~(lambda args - (define (delete-file-recursively/safe file) - ;; Ensure we're not deleting things outside of - ;; /var/lib/jami. This prevents a possible attack in case - ;; the daemon is compromised and an attacker gains write - ;; access to /var/lib/jami. - (let ((parent-directory (dirname file))) - (if (eq? 'symlink (stat:type (stat parent-directory))) - (error "abnormality detected; unexpected symlink found at" - parent-directory) - (delete-file-recursively file)))) - - (when #$declarative-mode? - ;; Clear the Jami configuration and accounts, to enforce the - ;; declared state. - (catch #t - (lambda () - (for-each (cut delete-file-recursively/safe <>) - '("/var/lib/jami/.cache/jami" - "/var/lib/jami/.config/jami" - "/var/lib/jami/.local/share/jami" - "/var/lib/jami/accounts"))) - (lambda args - #t)) - ;; Copy the Jami account archives from somewhere readable - ;; by root to a place only the jami user can read. - (let* ((accounts-dir "/var/lib/jami/accounts/") - (pwd (getpwnam "jami")) - (user (passwd:uid pwd)) - (group (passwd:gid pwd))) - (mkdir-p accounts-dir) - (chown accounts-dir user group) - (for-each (lambda (f) - (let ((dest (string-append accounts-dir - (basename f)))) - (copy-file f dest) - (chown dest user group))) - '#$(and declarative-mode? - (map jami-account-archive accounts))))) - - ;; Start the daemon. - (define daemon-pid - ((make-forkexec-constructor/container - '#$(jami-configuration->command-line-arguments config) - #:mappings - (list (file-system-mapping - (source "/dev/log") ;for syslog - (target source)) - (file-system-mapping - (source "/var/lib/jami") - (target source) - (writable? #t)) - (file-system-mapping - (source "/var/run/jami") - (target source) - (writable? #t)) - ;; Expose TLS certificates for GnuTLS. - (file-system-mapping - (source #$(file-append nss-certs "/etc/ssl/certs")) - (target "/etc/ssl/certs"))) - #:user "jami" - #:group "jami" - #:environment-variables - (list (string-append "DBUS_SESSION_BUS_ADDRESS=" - "unix:path=/var/run/jami/bus") - ;; Expose TLS certificates for OpenSSL. - "SSL_CERT_DIR=/etc/ssl/certs")))) - - (parameterize ((%send-dbus-binary #$dbus-send) - (%send-dbus-bus "unix:path=/var/run/jami/bus") - (%send-dbus-user "jami") - (%send-dbus-group "jami")) - - ;; Wait until the service name has been acquired by D-Bus. - (with-retries 20 1 - (dbus-service-available? "cx.ring.Ring")) - - (when #$declarative-mode? - ;; Provision the accounts via the D-Bus API of the daemon. - (let* ((jami-account-archives - (map (cut string-append - "/var/lib/jami/accounts/" <>) - (scandir "/var/lib/jami/accounts/" - (lambda (f) - (not (member f '("." ".."))))))) - (usernames (map-in-order (cut add-account <>) - jami-account-archives))) - - (define (archive-name->username archive) - (list-ref - usernames - (list-index (lambda (f) - (string-suffix? (basename archive) f)) - jami-account-archives))) - - (for-each - (lambda (archive allowed-contacts moderators - account-details) - (let ((username (archive-name->username - archive))) - (when (not (eq? 'disabled allowed-contacts)) - ;; Reject calls from unknown contacts. - (set-account-details - '(("DHT.PublicInCalls" . "false")) username) - ;; Remove all contacts. - (for-each (cut remove-contact <> username) - (username->contacts username)) - ;; Add allowed ones. - (for-each (cut add-contact <> username) - allowed-contacts)) - (when (not (eq? 'disabled moderators)) - ;; Disable the 'AllModerators' property. - (set-all-moderators #f username) - ;; Remove all moderators. - (for-each (cut set-moderator <> #f username) - (username->moderators username)) - ;; Add declared moderators. - (for-each (cut set-moderator <> #t username) - moderators)) - ;; Set the various account parameters. - (set-account-details account-details username))) - '#$(and declarative-mode? - (map-in-order (cut jami-account-archive <>) - accounts)) - '#$(and declarative-mode? - (map-in-order - (cut jami-account-allowed-contacts <>) - accounts)) - '#$(and declarative-mode? - (map-in-order (cut jami-account-moderators <>) - accounts)) - '#$(and declarative-mode? - (map-in-order jami-account->alist accounts)))))) - - ;; Finally, return the PID of the daemon process. - daemon-pid)) - (stop - #~(lambda (pid . args) - (kill pid SIGKILL) - ;; Wait for the process to exit; this prevents overlapping - ;; processes when issuing 'herd restart'. - (waitpid pid) - #f))))))) - -(define jami-service-type - (service-type - (name 'jami) - (default-value (jami-configuration)) - (extensions - (list (service-extension shepherd-root-service-type - jami-shepherd-services) - (service-extension account-service-type - (const %jami-accounts)) - (service-extension activation-service-type - jami-dbus-session-activation))) - (description "Run the Jami daemon (@command{dring}). This service is -geared toward the use case of hosting Jami rendezvous points over a headless -server. If you use Jami on your local machine, you may prefer to setup a user -Shepherd service for it instead; this way, the daemon will be shared via your -normal user D-Bus session bus."))) - - -;;; -;;; Murmur. -;;; - ;; https://github.com/mumble-voip/mumble/blob/master/scripts/murmur.ini (define-record-type* murmur-configuration @@ -981,7 +305,3 @@ (define murmur-service-type (service-extension account-service-type murmur-accounts))) (default-value (murmur-configuration)))) - -;; Local Variables: -;; eval: (put 'with-retries 'scheme-indent-function 2) -;; End: diff --git a/gnu/tests/data/jami-dummy-account.dat b/gnu/tests/data/jami-dummy-account.dat deleted file mode 100644 index 0e908396ca..0000000000 --- a/gnu/tests/data/jami-dummy-account.dat +++ /dev/null @@ -1,392 +0,0 @@ -;;; -*- mode: scheme; -*- -;;; JSON extracted from an actual Jami account and processed with -;;; Emacs/guile-json. -(define %jami-account-content-sexp - '(("RINGCAKEY" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3F\ -oa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzBxWUozSkYvTzhQRGEKRnUwRnpRcHBCaD\ -gybGJMdURrNTlVU0I0MUJSaS9kdDZGV1BRN29YOVpsY25vNGZzM2dmUHQ0dU1hRVBkVFBGKwowbGN2Q\ -jc2cytQTEFlcjlOZGpVQzQ2ZXp0UnNiNE9aQXc4ZUk1M3EwSU04QWJFd0o0ZjllLzBmQUFueHgrK3Qw\ -CmZDeGV1YTBUaVBqVHBpZVJMNmpwQkd5UGI2Qk5pU2ViTkZCNzJOMTBnbzI4REVQYlhkNE9CNkN1blZ\ -5RGZKSU0KOC9PRy8rMndNamI4WkRwT3JrYy93U2ZHbnQyZXA3U0xKSkgwOFIzV1FKWklsSndrcGdLTH\ -FRakVwWFNpclN4dAozSkdtdXdBdE9LaXFFTXh1R043elV3ZlNINEtHdkRaUFNkZklZVXJ3eEp4aDJZZ\ -3lobG5RRC9SSnhRN3d4YlJBCjFhMUZVV0FzbDhLODk5cEtESk1GL09VOWZMRUx6QlViblpaRDRmSlg1\ -NmcyTlluUnJobS94NG9FbFk0MFNYMUUKcHYzb01hNnZrVGN5RjJnUFhKL2FkUVJoS0dFaGRjaHBpeDl\ -5UVphaDFCUFBGYW5jNzBMcjhOaDZJeHFNQ1hiMQozMG9vWHpWZmZNMVFOd28rL3hzRnBlRkRqUTAxQ0\ -9pdWZocitKREcyc0txb0o0V0JwYVhubWI1YXVrVWUvV1RKCjAxVmRyaEkvSVExd3V4QzNMMnpac3dVU\ -1NTaDk0aXg1M0hpU3pWbkI5UkxmaVhZUUVCcFEyNHVoRTdiYlo0bm0KZTczZC9zenpPTXMzYUt3OWtW\ -a2VLMTVtYWhSVWZjdEdhSVQxTkhGWUNYYXByaWExakdNdVpmSk1pSUtZUzNidQpMbUhZckF6dEptNDZ\ -0aHpjdnN3NHlhMnFoa2xUUlFJREFRQUJBb0lDQVFDaHZaUm85KzZ5aFhFTHZ6U3FXZHcxCkZGOERibG\ -hIMmhVWkNuV0kxMDM5SmdyRkxMczFSU1krSzg1aFZYMk9hV1VTNk44TmNCYzUyL1hrdFltS09HUFQKM\ -WZqMnE2M3pPcDNSSFdGNWVPMXhNeExRN3JZSDhqMGZZTFFTUytKemdwb3ZRVnJLSXkrb21JSSt3aUN6\ -R1laRApGQUM0ODJzL0J5MHdtRjVjdC9JTEdIeVY3ZXNVUlo1Vi9iL0ltQzUwQ1lDUWpQR2xBb3JkeUx\ -1MHp2NjZZUXc2CkQycTg0VHAyVUg3SExEVmhFNytUbDg4Q04xWll0VGtpSkthbkNpMFVmbStPKzJFM0\ -5HM01hajk1aDl2NktqYkoKUlkxeTNDRTVmQmkyUFNLbVVzRjN3SzdhbzJDRks5MTgybmlxL2FaNm5WO\ -Xc3NmVrRjhEOWUvS1pqUE5ZT0xkaApFczBSL2laV3RpbUx2RHdXQWNWNFNnSFFjNXJvNU9yOEFUS1ZK\ -VmlzZGFuWXkvdUhmVXZWN3U5cDVLK2E4SHU2CllabW13ZTh4bnF5M3V2M0VabE9LY20zTnZvWllVMnJ\ -HUUFQQW1sWWQ4WlRsZGxPa1JCSGxxYzllMmJuSnNTQW8KNUhhS0N3aDJsWmZpalVGNXFrMXNQcm1kN3\ -BlMld2VVV3QmVuSjJnS1ZoTE5VVGtHWmtTWGhzNlV3WWRRMjVtRQppQzl6WjhXNkQ2OXBvb0lsTTVXT\ -01ySEs0Rng1ck9vT05kUHQ4NEk1bTI3cnpnbXM4QnJXVUlGLytZZjJ0bGdmClBIR0V4c3ZCK3JRQk52\ -WHU3dXoxcVdFTlJTL2YwR2E3dVF4ZW5sZ2dubHc5M1pNOW1GWXpXb1RpdWFmdnphTnAKWEsrTEVrV2F\ -RYUs1Q0VaNEhmUlhBUUtDQVFFQTUyK240OUxQODlyQkR2bFdsTkxNanJqTDdSb0xyQ3FVZGpQWQpyT1\ -hZS3ZkTkxyS2NTc1hNdkY4YW5HQng1UG5oVDZGY05ic2dzQ3BUUXowMThZYmcrbUUxQno5ZTdFNTJGZ\ -i9NCk9BbWZqSllXUnZueUtiNnB3SGlvOHlXUXlVVk1zZU1CcmpvWk1kNkpPZEZ0K2JITHBWOS9iSkdR\ -a3NTRE04WTkKbWxGQUlUL0gyNTh1K1ZKTWsrT0prU28zZmJQSk5Ja1Q2WVBKVmNaSnZTRGI5QU83WDF\ -lendCOXVRL0FEblZ6YwpSQkJOUVZaTStZS2ZNWFJBdmFuWnlmWFFwaUxCQW8rRVRPSHJCR1dDRUhtSF\ -RCaEZIMTkyamtxNlcrTStvS0R1Cm1xMitMc2hZWTVFc2NpL2hPOVZjK2FCM0hhaGliME03M092MHFNc\ -WZoTncyU1BncHdRS0NBUUVBeDlaR1gxQnQKL3MwdGtNcGV1QWhlWjFqTklnZmFEY0RRTWlmU0k4QjRx\ -WUhiL1hOREQ5NjFQME9zMDdCN2wzNE1iZ3U2QlNwcwpXdSt0Y1hjSFlqQVJUc0Qzd0pSaVRIb0RSQzR\ -YYkxEa2pHRUVCVzRKbFVqZTA1QWZrU0QrdkZSMkJwZStxQlBLCm5yb3Mwd1BWL3RXa3MzY2VFOUlBTV\ -pWWDhQeFA3RVNXbitVZDJEWkZhcVFLb0JybHZXRXhxelpYUEJSVjhoS3QKcFBqWnFkZXFQLzhUZTBtS\ -zh5MEVreXVXOWhFdGZ2Sm5HWXhMNStrSG9xd2hQVk1tODZ5YlZNVHRQYWJTdCtPUwp0WHhJTE9RMWRN\ -QkFabzRxSnNkUUZJcTJnSHA5WFYwa2ZNUms1ajdJT1Q4c2Z6TlpKVkRNK1k5VHVlSGJXSnduCnZsWld\ -VZ2NVZTlBaWhRS0NBUUVBbFJaK2h1ckUvNGdLR2dWUld5bTRrTEJHM2dTTFJHdGhuQXVtSnlzaFoveE\ -wKZ2l1Wk55bll5L2hRQWpDMjdoUnlxb04rRFRid3hjdGVPOUJ3c1poNzBZOVJROHYwOERGVExMVE43O\ -E56UG5OcApBbXY5TGhzZTYxaFBMZU1qTkNVcVZPV3hyWFRMeWk1YkpCM2Z4SnhlWGJmNU5BMUpudUpz\ -eXF1SC82TWJ0cytKCmhkY3p3WFRjMCtBZVBKOS9nOENQZXdKYkMzRFVBQ2R1VlNHWHo4ZWZxcm1xbDd\ -jbnB5ZzBpK2pJRkNpVU8rVEcKVFcxeDg3KzUvUFF2MGtSQ0Z1UUloZ2ZCNkcwWW9vcHBrUWRZdXhKZl\ -pPaHdUUldpbTVMMlF5K294WWZySGVQOQozSlltbGFCMmJiN3kxL1FoQjcvek9VMk1nTEtYdHl4Z09ve\ -EpoQlFwZ1FLQ0FRQkIwSUE4dy9CMkNuMEhRcDhQClhUSTZOelRZRUYzd1NhQkg1SFdBOE5MTWdNaERJ\ -TUxsWnlPcVFrK1pLSGFMM2llWjFxTGRNS3VmQjNESC9idWcKeXRQb2JBVXNsN0lJSGVjVmZWaVpvMml\ -pRXhHUCtEMlB2UUFtRFVGWU90V3FrT2FPSlV2VmJ5ODhOM1NyeW9lZgo5aHpZUGxMWmxFQWNGR055S3\ -FibjJXOENHaU5LSWhXYW1Zd21UclY3T1pkeUcrTi9GZk40Vms1NkZyc1pCTDQ5CmRYU2xGZ045TTBaZ\ -WNleTEvZEpPRE9lSHNuME5VK0gvNFZEUk1hR1NmelpwSkxJOXE4T2FiSWpVM0ttb24wQTcKdzFWeWNU\ -L1FwYlBxRUFVckt5dytvMzV3MlAyaUZ1czZiMlBvUUxFTGFTRVl6K3R6UEw5UTM1ejNRdGdMQytuagp\ -IUmxCQW9JQkFES0Q4NGhrYkphczlIQ00zanNNSU9kb213ZEMvcktxVUxKNHFWZU5QR0xDY2VpNEdocn\ -dlQnNICnNoN0hibFlZSDN2U2Y3RW1iZEVCb2xlWVJsaUx5ZzJDQU1ZOGpNK0lpUEwydk1yM2Y3SzZPT\ -mU0UkVPUFJSZkcKWlJDcTh4a0ZPQlg1SUJUbkVCV3QzdVdyb1NGY2x4RTdUa2I4VkUyVzExTG9ZNlkw\ -TUNPaHdxN21xYXRUVnNrawpTRDNySmkrTFR6a2Y4OEx1bjZZNjdiaFNOTWpKZkFaUXNQc0FTRkJBUTJ\ -rQnE5alRLZGVuaU4yYTJIbm0xNCtrCnJDeU9ZVE14Q2hQbWNpS25pVy9MWnFUL0U1dlNRUGdBVzc0dT\ -VLazJoSjRBajNjRW9NVEwxSytZbStWYWh2U0cKTi8xOFdYQ1JRQkg1d0p2eXJYczBtT29GQlRnTWg4d\ -z0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=") - ("ringAccountKey" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTk\ -Jna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRRDNCdDRnOUVUdk9EVnYKM3hWV0ZlS\ -1Nnbk5uVEF3S2dYa3IrQ1FhdU8vTGZWM01RenNSNHliL3hoaWhhb1Z2c2FtZ3ZRU1haL0M1R3I5QQpE\ -YlAxbHNHclRCK1pXMC9uMXVEb3hmVWdZRWY3SGtVanJtdVFjUGlFWGlUVkNiY002U0NzdVNrMnRxdE1\ -BNjBNClRacVo2LzAvbkEvblR5TnFNaUJNSmFRN0NUb2xOTTh2Z2tvd0tBRW14OGpJUG9YZEttMnd6MD\ -Z6SnhwU1d4WjUKc0FBTkdSSHU0b2ZXNWJiMERvamtnRzFBYUJ6Nm9uSmdKK1JFSWV1UkpNQWFHYmtzW\ -TQ4Z1Z1b21BVTU0UFNvUApFb3psVGdHd1k0cnhJTGE0V1Y0RVZMVExrR240ZTYrRUYrSjIvRURBUFdN\ -bXREdFpUeURCc2t5bStLaDRUT2xPCjdnN3JlUUhLdDd1R21YU0RnbzVKZ2hOOHNVRFUxL1Z2YmFFcUZ\ -tTDJrZWNGOVlVZmNsUWRGY1ZncmIrMkh5R3AKRVc0b3RkZjlYYzhOMWxrbGk1dFBqRGZuQ1U3OHB6QT\ -BxQmV1SWhZTnF1VjhGSm1NemhXeVFDbE1MUEFYbXFVdwpWYlY0MWduM0NkektuUlVhZXFONXlzOG5KQ\ -mRJNDBleWlYRUlvU0VKcFpyT1Z5ck1icnNCQzltaVFpZUFhSnlBClFvcjFaRGlwMkpZNUFza2phUGQ0\ -NGk0MGNkeFpob1RhNnpzako5UjZScllFYjhWTmZQemFLaElwSXJSd2NpbCsKWjFrbUUwSE1kY21ITXR\ -hbWZhK0l6WUR4dDV6OGVOZys1RGpVVG1MRkdyQUVWRW5hNDcxdllxYnk5UCs1T1cvNQpSdkxtUER2Tj\ -dlUURpTlZENlBYRFk5OTY4bTZaaHdJREFRQUJBb0lDQURjOEkrTCtlNE41OEFqcHV0MmEyeVNqCllxY\ -VFUSWowMW1GTWhOWXMwQUdTTUswQncyMkdleXZwNFl3R1EzdnNIOSsvSkEydXdoYkJzazNpUW9FQmlx\ -Q0EKenZmOWdPcDRFNlk0elV6RitwSmQvRnUwSG4wWHBab0Rhdnp2eFN4djNFeUN3b0puYWZuL1FHeGw\ -xZEhoQUtsKwpmZGZjekRCc3NPZ1Y2cGtBd1MyY2wwOHFOT2g3cVhaQWFkYk1sQ1lWM0owU1hhaVZiNz\ -lHZXNvTzNwUVBMUUZiClNjQjFjT2sxYnNxWkpOU244d0xmMis5QVBEdzMwWEtNNHg5eTdRTE42Q3oxQ\ -WpvcFJLQ0NIS3R1SEc4UmVETTIKcnRTbjJmTnltQ0VqeDZGVTB6MHFldDV3Y01UbU5weEZuYXdEMU5s\ -dFpnZXBsSllwTjVKZXNEUmo2cFlnWXBPKwo5UDU2cEdtZVNTVzVxcHRoWFFLQmFsdy8wWXp2YUlYdHp\ -hTnZyNUJzRFNrWkU3cldzODIvQmFzUE1RckFmOGpLClZFMU9pSzcrVllUVTRFVWVoZ0FZOXdzNjFqYk\ -lSSEpQbW9VQXpkWHcyL0d0Vk9JUTFwUFJnOXNYN0JaMmUyV1YKdmd5aThPUEJxRWtwblBiMkU5Z0d1U\ -25rY01OeWFVbUl0c3pFandadW14dnZrUW5HanlTb3pjY2R3dnNQNnBJagpoN0g5VUNQTHdOM0N5N1lp\ -UmliSlZBWlNjZkF6QzhubXNLODQrTzJUZHBzTXk0SEZDMmM4dlZiclpteTVkWC9qCk1ESnBzS25JWlZ\ -JMmpXSzRpRS82aUdIWVdoY3JvWnYvVEJ0YW1SQUxTWDVOYkhhWTI0bXVRSG5yMldtaytld3EKbHRGbC\ -90bXgyVkpWUitMZ0JCUGhBb0lCQVFENEI0MjQyVTgvbkJ4d2RzelhCdWxBOVFTa0I5Zktud0RlRkV3S\ -wp3Nks0eU14YVdRU04ycjRxRDhLcW93OVZVMzRYdkRWbFh0RUlDaVh3Q0hZdW5IL3g3cXNSdEVzbHJM\ -aWg4UHRPClpDSU8zUml4RmlIQXFlQUh2YXF0NVhXdndaREx6WnV2THRJOTdINkZ1QjYxck5qMnhxdlR\ -IN05pUmp2S0R0WXgKR1VtNURoQ094cm9tR0NkWHRnWHJGaS9WRU1TSmpQMkM3OTNrN1pTNmNZL0Nkc1\ -RqWEsrZ1UzeWM3OC9kN1pYbwpKMGg2WFlSdmhlQW9Bd2dkVTl4MWtYL2ZmY2tZK1hUcFRwV2xXWmtlU\ -ytsejBpQkRnUlJzMm45OFZDeTZDRmRZCldsZXZaZy9SWXZ6dzlKdWFVcXArOHpHbHNXR2xuOEhtZW5X\ -Q1luSHJnNFBxQnRkL0FvSUJBUUQrOXhDL1N5ZGIKVWZxMUJHYy95YktZc3RLb3A1azB0d3M0SlUwTzN\ -aT1U3MlZ5YTdLT2lTemdPSzVnL2QzckhMMXR2dHViTDBWNAo1dEF4a1AzSkVYbmxZZU5IVkpROTc2RH\ -NGS3Ztc0FGL2JJdVBsdFBRT1dyM3g1eW1RU3lCOTBUczV0dFdWMTVQCktYYVNnMTZidDhwNS9MeERkZ\ -ng2c1YzN1o5RDFRd21EQllreVFIcWQ4clljTm9ad1M5ZnI3UTZhN1ZNSDVtT3IKbEF5dzBCYVdZQk9k\ -bjFGd0pVV3NlRlpmTy9vNUVqZk9Hd0xMR1hiOEVmQ1hqdlRYcUNHLzNrT1JvN1NkOWY1eQowVjIxMmt\ -YVVNINHNDbFB0SmwzeHpaMWJxQ2RMVDNITWNLUTk5UGFFVFppQnNXQ1lOcXg1c0Q0RGhoMzdZQ2hKCm\ -hlN3VUM1E0MElINUFvSUJBREN1VXR1b0UweloyQjhld2grbUpKdnlPMEh5cENFSnlrTE1Xd3gxejNkV\ -E9nQzEKbmhZMWk4TjNxbTZSYUk0SHdDVHFkTlI3b3ExZ1NJZnZNVHIrem9IdXBUYnBXeUorM3hJeDJU\ -Rk9wL3lnMnByUApURHFqWE94SUJycnc0WU5vaTRIa3poeTVKTnl3a1RpdnBaOWsySVMvQTdTQmNWVGx\ -raENianVDK0pPRWthSTJOClpiWGFZY1p1WElVQ3FzcTM2c3RRbCtWZUxRQWt2VjlHc0wrclRnT09Dbz\ -UrTkdRZEVZQnVoRkMzZlJzL1JhSVoKOWFBRTBFL3BTTWp1a05tTnQ2Mm1NSk1tTUdydXhnWFRRblBRR\ -npNSW43aXB2Z0hxQjRsUDM4emdsbnMvbmZVcgo1NWRuZXk3ejhMRFFETHVIc0RHd3hINzNKQjgrTVR2\ -WGFVbkNwQU1DZ2dFQVNBSGxBL0dvdXR6TFRvWmcxcDRUClI1YnhjZHBycFh5d3VYbW5hclJmY3VldG9\ -nUVNtTGpiS0xRNVk0RXZSTENJTzA5MDNENGNnOG5FTU10L01XTXoKSnZwZll3emJGU2J4THR1anRQSX\ -VhaHR3eXV2UkJIVEM1aG5FL3h0WEE1bWZLTDBHWXpzbmtubm1WL2lzSnBSZwpwZFVnSW5sWEJodkRyR\ -FlreUsvWEp0N1FZWlhlUzI5NXlUd0krZndoamlzVVBlTWEyUmRUUE9rQ01JbUVaNUhZCjJHSmZjS25H\ -SkxDVHpDKzNPcGtQazdFRE4vTUlMS2F3YVUxaGp1cVlKWVVUVmpXQzFEM2VUL1ViWHptM0VQNHMKVEN\ -uYWpCYVMzN0N2YVd4ek5JektXZS9TSXdGbEFmYWNSTHlneUR4Z3Q3bHp1akVObEtvU2xya3h3ckpEND\ -Z2WAptUUtDQVFBcTVQWWxSQjgvNnFiWWt1OTA0NUZRdXk2QWtlYXBaMW0raS9SQzZtbFRvUXB6NDlPU\ -Gs4ZGx5YjVtCndvSVhpaEo2V05jN1RsWlRYMnpQTTRBS0M5VFNBUWJrWDg5bjEyU2VDSUlHbXVINnk0\ -TjZiY2lxZjVVcSsvc0IKcHJKeFRNYlRSUFpqS0VVd1N0SFg1MUQ1bi9sQnZERGY3Y2VEZytsYlE0RjR\ -KMTlPd09oZ1lGcjFheGQvNXd2VgpURjNoVlQwbFZGN2RyRC9iMHZOcmxnbUNjbEk4UDg1a2dkRUhZbG\ -ZtTFoxeXJIMkNXVy9SS0lsWk9ZdFVuNFNpCkp5a2VlNDROWElXU3ovalRBdFRta3VQTzRvUjF5d3dRc\ -jdhUTF5a3hRVm9rVm5vY2xqU0tyQlk4R294a0I0eDIKUDNrb3F1UnkvcUd3QzBnN1o4ZjBTQjNQZVZt\ -eQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==") - ("ringAccountCert" . "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ\ -0F3SUJBZ0lJRm1tNmZuaWRndEl3RFFZSktvWklodmNOQVFFTUJRQXdUREVRTUE0R0ExVUUKQXhNSFNt\ -RnRhU0JEUVRFNE1EWUdDZ21TSm9tVDhpeGtBUUVUS0RjNVpqSTJaVFZpWTJNeU9EWXlPREppT0dFMwp\ -PRFF6TUdOak1EWXpNakV4T1RFNFkyWm1PVGd3SGhjTk1qRXdOREUyTVRjek1qRXdXaGNOTXpFd05ERT\ -BNVGN6Ck1qRXdXakJKTVEwd0N3WURWUVFERXdSS1lXMXBNVGd3TmdZS0NaSW1pWlB5TEdRQkFSTW9aa\ -k16TkRWbU1qYzMKTldSa1ptVXdOMkUwWWpCa09UVmtZV1ZoTVRFeFpERTFabUpqTVRFNU9UQ0NBaUl3\ -RFFZSktvWklodmNOQVFFQgpCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFQY0czaUQwUk84NE5XL2ZGVllWNHB\ -LQ2MyZE1EQXFCZVN2NEpCcTQ3OHQ5ClhjeERPeEhqSnYvR0dLRnFoVyt4cWFDOUJKZG44TGthdjBBTn\ -MvV1d3YXRNSDVsYlQrZlc0T2pGOVNCZ1Ivc2UKUlNPdWE1QncrSVJlSk5VSnR3enBJS3k1S1RhMnEwd\ -0RyUXhObXBuci9UK2NEK2RQSTJveUlFd2xwRHNKT2lVMAp6eStDU2pBb0FTYkh5TWcraGQwcWJiRFBU\ -ck1uR2xKYkZubXdBQTBaRWU3aWg5Ymx0dlFPaU9TQWJVQm9IUHFpCmNtQW41RVFoNjVFa3dCb1p1U3h\ -qanlCVzZpWUJUbmc5S2c4U2pPVk9BYkJqaXZFZ3RyaFpYZ1JVdE11UWFmaDcKcjRRWDRuYjhRTUE5WX\ -lhME8xbFBJTUd5VEtiNHFIaE02VTd1RHV0NUFjcTN1NGFaZElPQ2prbUNFM3l4UU5UWAo5Vzl0b1NvV\ -1l2YVI1d1gxaFI5eVZCMFZ4V0N0djdZZklha1JiaWkxMS8xZHp3M1dXU1dMbTArTU4rY0pUdnluCk1E\ -U29GNjRpRmcycTVYd1VtWXpPRmJKQUtVd3M4QmVhcFRCVnRYaldDZmNKM01xZEZScDZvM25Lenlja0Y\ -wamoKUjdLSmNRaWhJUW1sbXM1WEtzeHV1d0VMMmFKQ0o0Qm9uSUJDaXZWa09LbllsamtDeVNObzkzam\ -lMalJ4M0ZtRwpoTnJyT3lNbjFIcEd0Z1J2eFUxOC9Ob3FFaWtpdEhCeUtYNW5XU1lUUWN4MXlZY3kxc\ -Vo5cjRqTmdQRzNuUHg0CjJEN2tPTlJPWXNVYXNBUlVTZHJqdlc5aXB2TDAvN2s1Yi9sRzh1WThPODN0\ -NUFPSTFVUG85Y05qMzNyeWJwbUgKQWdNQkFBR2pRekJCTUIwR0ExVWREZ1FXQkJUek5GOG5kZDMrQjZ\ -TdzJWMnVvUkhSWDd3Um1UQVBCZ05WSFJNQgpBZjhFQlRBREFRSC9NQThHQTFVZER3RUIvd1FGQXdNSE\ -JnQXdEUVlKS29aSWh2Y05BUUVNQlFBRGdnSUJBRDMrCjlscFluMjZyeG5pekY2UkNvZFFFVmgvOVF2R\ -Wp1V3dHUWZqa3gxb3VlVjdDMzUyWnpIT2hWU3VGcG43TUxkVFcKamI2dWhMRkpoMWtlTDlYZ0pHalMy\ -V0UwdDlJUGp2UWx5UHIwRWJoWGRJNDJMYUR0NDk0dWQ2MEE0bWg0bW1zbwovcjY2NERKOWMwUjZBOUd\ -QZUJjWi9zekhMQjJ4VmM5M3hYNjVjcmNoVTFLMDBVK2ZZUWtHS0xidFFXeUZzdnlKCmRHdFRxamVlYV\ -VNeGt5dFFWNWFxVHJ1SG4vV3U2RWRZejYrZ0ViQXJHUURRK1J3QmMxMDNGcWRIU0xReWdEWHoKd0pNV\ -GhDREdBVmR6V2NCemJYL1JIZms5bTZzK01HblNJUFBrNG9FOUdFbGdQc2JUZ2FrTzc3aElUdzRxZXJi\ -TAp2M0tiaTVOeVZZVFNVa3hDb0VRSkIrWlZJT3BVNFhRdVAvNkZkWldBRk1LU0Jkd1JLcnRmT1hZT1E\ -xcVBvYU1uCjZPR1VGMU0rYWZ0dTNCMkNhZ2ZMaE5hbVBoSjdxWTNSMzJhK2VRVllvYWlESXZKMElIUH\ -FnQ0NuWXlIaVBHTjQKS3VvaE9IZFpXTkxOUXdIQTg3SDV5NEwrSGZISmdFZFppdWYrbTNMa3JJcnN5M\ -WFoUjgyaEpZVkhreHVyZVRDcQpIR2NJaUIvTHpkSG91WFVrWGNrNjRvWXVRQnM3ZE9KVEx6bGlibU8x\ -elRzVWRoc0d4dE9zc2lYWkFjUURmZHBnCjNHdFJ3UkRwd0ZtL2k5TC9UWjMzMjBwY0VZd21aN1dBSzJ\ -Ra3Z6eWZlelRGeXdLWmh3c0RQRkwycllzLzhXdWsKRk1sek5BVmRMTytTS1RxeGlkM0l1elFaQ0FwU0\ -5ybTJMY2ZVN2grWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBV\ -EUtLS0tLQpNSUlGWFRDQ0EwV2dBd0lCQWdJSXV0T3MxUXBsSVRvd0RRWUpLb1pJaHZjTkFRRU1CUUF3\ -VERFUU1BNEdBMVVFCkF4TUhTbUZ0YVNCRFFURTRNRFlHQ2dtU0pvbVQ4aXhrQVFFVEtEYzVaakkyWlR\ -WaVkyTXlPRFl5T0RKaU9HRTMKT0RRek1HTmpNRFl6TWpFeE9URTRZMlptT1Rnd0hoY05NakV3TkRFMk\ -1UY3pNakExV2hjTk16RXdOREUwTVRjegpNakExV2pCTU1SQXdEZ1lEVlFRREV3ZEtZVzFwSUVOQk1UZ\ -3dOZ1lLQ1pJbWlaUHlMR1FCQVJNb056bG1NalpsCk5XSmpZekk0TmpJNE1tSTRZVGM0TkRNd1kyTXdO\ -ak15TVRFNU1UaGpabVk1T0RDQ0FpSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0l\ -CQUxTcGduY2tYODd3OE5vVzdRWE5DbWtHSHphVnN1NE9UbjFSSUhqVQpGR0w5MjNvVlk5RHVoZjFtVn\ -llamgremVCOCszaTR4b1E5MU04WDdTVnk4SHZxejQ4c0I2djAxMk5RTGpwN08xCkd4dmc1a0REeDRqb\ -mVyUWd6d0JzVEFuaC8xNy9SOEFDZkhINzYzUjhMRjY1clJPSStOT21KNUV2cU9rRWJJOXYKb0UySko1\ -czBVSHZZM1hTQ2pid01ROXRkM2c0SG9LNmRYSU44a2d6ejg0Yi83YkF5TnZ4a09rNnVSei9CSjhhZQo\ -zWjZudElza2tmVHhIZFpBbGtpVW5DU21Bb3VwQ01TbGRLS3RMRzNja2FhN0FDMDRxS29Rekc0WTN2Tl\ -RCOUlmCmdvYThOazlKMThoaFN2REVuR0haaURLR1dkQVA5RW5GRHZERnRFRFZyVVZSWUN5WHdyejMya\ -29Na3dYODVUMTgKc1F2TUZSdWRsa1BoOGxmbnFEWTFpZEd1R2IvSGlnU1ZqalJKZlVTbS9lZ3hycStS\ -TnpJWGFBOWNuOXAxQkdFbwpZU0YxeUdtTEgzSkJscUhVRTg4VnFkenZRdXZ3Mkhvakdvd0pkdlhmU2l\ -oZk5WOTh6VkEzQ2o3L0d3V2w0VU9OCkRUVUk2SzUrR3Y0a01iYXdxcWduaFlHbHBlZVp2bHE2UlI3OV\ -pNblRWVjJ1RWo4aERYQzdFTGN2Yk5tekJSSkoKS0gzaUxIbmNlSkxOV2NIMUV0K0pkaEFRR2xEYmk2R\ -VR0dHRuaWVaN3ZkMyt6UE00eXpkb3JEMlJXUjRyWG1acQpGRlI5eTBab2hQVTBjVmdKZHFtdUpyV01Z\ -eTVsOGt5SWdwaExkdTR1WWRpc0RPMG1ianEySE55K3pEakpyYXFHClNWTkZBZ01CQUFHalF6QkJNQjB\ -HQTFVZERnUVdCQlI1OG01YnpDaGlncmluaERETUJqSVJrWXovbURBUEJnTlYKSFJNQkFmOEVCVEFEQV\ -FIL01BOEdBMVVkRHdFQi93UUZBd01IQmdBd0RRWUpLb1pJaHZjTkFRRU1CUUFEZ2dJQgpBRWFDblluY\ -m9yWmRhWWZRUmxSb0dtSE94T1g5VFdNMXY0dWEweCtFcG11RDVWRDVlWEY1QVZkMlZadEdaeHYvCkd2\ -VnFBU2l0UTk3ampKV2p2bURWTUZtb3hQSmRYWDFkYTd5cmJYeFRmYm1mM1pac1RpdmZVdWQxYThxdUN\ -3dTUKTnBrdHFjV0JyMHRnNzFhOXlidHJOdm1GczZGRE1WMXkrY2JxYlp1UWlDWnc0WmZhekFaeTRQRG\ -NocE9SNjRCSwpBWFZIT05HcWFoV2hwcDkwd2E0TFEwUTE1U3FDR25kYVI4SHg5MHJOeEdkRjM1T1BLU\ -jd1TDdrWDU4ZGxaWStDClJJK1pKMndYMzJUZzgrc1RtTmNaUTliWDdvS0t3R3E5Uk94SjZJSUhOSnN2\ -bXhaSnlPcmE2N21hNTd3OWxiQnUKSzJlQ3cwZjRDeHdLNU1LNStkbWx6R3dhZmJlMG00TTBvVjlhWUx\ -ZTzAwR3Iza05heW9PdjRRVGtEM2pCMzVSMQpDMGJnQmk2eU1sTVJ2akZ5eEZkOFJpL003VG1jcXNObT\ -B5aklqbkZaenVtMFZTR1NLMXlRU3Flak40S0Y2R0JMCllpZ2JpM3c4WG5HYm9pZGdBUE9ncVVJeTJ1d\ -EU1MllzVXFsVHVncXhtM2xDOUhzaDM2UFJLNURDUG93eHVUNlgKcXo1M1ZiN2h6TkxLelpiRlJzbUdF\ -OFY2cWM2bXZTbUFXa25nL3QwaStXVmdGVkZuZFQrQ0oyNTJsa0ZacGljdAp6ekdETW44VUNDRUp4TDR\ -KTklTM2lLOUhlRys2MlZuay9QOEM3YVpLSXpVdjFud25rcVdUUUFYWDBKckJGdDdICjI5ZDk1RElmRT\ -RuT0FyS0JFNHc2Z1R4SU1uZzVzWi9ZbDFjcG5wUHlsR3VICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tL\ -S0K") - ("ethKey" . "fN8cOT1lYNziaW0+pjBIgZ8r6+zMMhHsukkWBNPDsFo=") - ("TURN.username" . "ring") - ("TURN.server" . "turn.jami.net") - ("TURN.realm" . "ring") - ("TURN.password" . "ring") - ("TURN.enable" . "true") - ("TLS.verifyServer" . "true") - ("TLS.verifyClient" . "true") - ("TLS.serverName" . "") - ("TLS.requireClientCertificate" . "true") - ("TLS.privateKeyFile" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQU\ -RBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzM5b1Z0cXNtUGdaSUgKcHpTV\ -GtlT3BlWC9CSEx2KzFTYnJPSFpVRHEwNFZCUU5BNmJmSFNSWTJpbHE1WEVheXNVSmwzQmsvM0txZEhS\ -cQpEV01wQ1dpcE1Vc2FwSGxJR0tSWHEwbXhQZ29WODZSUVBub1dCRTdhWVVEZTlJZXlxMmllZXpDK1l\ -YSnBtWTljCk5tblpaMFlHOHJGMEVpWFA0SHpVWGphZklTKzdKTTJ5ZTZyUlpINXBvdHBQNmV4NXhqVU\ -VuNEFFdWhuWGJ6U0EKNnZMaC9wYnhIUVBHMVlmNHFhcU9TY1lXV0ZWaE5ySVBCVFJNZ2RaTWRGRTh0W\ -VFyaUNkNTV4dUhrYncrQmY1VQpmVENqN25tVEcybDJNbGcrSXBHVkFXUFRWNkl0NFNiS3VadW5MZmRD\ -S2FrSU03SnA3V2dJWjZNRHdkaFFQWjJNClJPQzN0b0c1ajh4aFZOU0Q5QlZiOGZBcnRmeHdldlliQk1\ -yOEdRV2JQYlRKYk9tZHZnUGRGcXNTb1F4QnRUcWQKSG1TMG53RjJ4UGZrdm1Cd3BSWWhHNHpuZlVtMl\ -BQcDBIK1g5Sk96d0R1QWI5K0c0cWZtK3Z6bDd1N0o3anJ0TQpHQlJlSlREdGFNd2RHa0lmcUNRZGREZ\ -HFpM1hzVHVVOVNubVN4NkdxT2V6ZU04bk55dmtZWjR2Uk9vODFzU1JjCjQ4L3pZZjJ1OFBJUm4vVy9x\ -Zk9hbjVIbkcxeFNBUDZkcHAzQ1Y0YTgwUFduZyszcXQ4NW5Ha2k2Nk00NUlnRjgKNHZwRGUyM2YyZnp\ -rTk5OOTk1VTBvUStKVGg5eG1HaWJmenlCaXk5d3RUVHVYWDB3Y3gwTm9wNjdYaFFDVVBGSApybVd6Wj\ -V5bXhEdkRhcUN1U3NwcG0vTkRVRWQyc1FJREFRQUJBb0lDQUYyWXo4bzhXdERvMjZPSkx2Ym1BeTcyC\ -jRra2VsWWZTYXpyQ1AzSUZCWnpqS2xCMHl6STVZWVRUZXI4b2ZhTmtCMXdaOE5WeUlxVVhHeVBhSzls\ -MU1BcmwKU1pFRW5iQlEyVXpCbVh6VVU4MVhhUUpxeHpMc2ZqSWR5U09teG1QaFVobFFGRHpJMTYxaXM\ -4MzI0V1A3WjJXaApsU2U1RkFQdjg1TVpYREVhY1c2R0N5SUVTYVMvdkpHQ2loQ2VzL0pCSmpoejdtNT\ -VRU3liSjl0dnJxUE5KSDhJCmhDTm1BUWhEU3NPYVJXNlpBdGV6UEdUb0FQUHNHMXhLMGdwUlVDM1YyR\ -C90bGRRa0VlSjhxaW5TaUN6ZjZIc3cKTnpncjVUbTMzTm96R3Rjc2Z4ZFl0cVB1UzRPRG40bktLSFpE\ -MTBLTng2Qi9HakdQTHIra21jUUVSMUV4U2s2QwpSemtzSTRzRml6VnVLYTFNK2ZqaVNVc3RTdHV1cTJ\ -QQTJJNzFNTTZlMHJmU3ZKM3VESEJYOWY5T1RFaGxZUFBkCnB1VWM2ek1pdEJKRTg2SFo1M08wR0MzdG\ -p1M1BnSnhmWnJyMHBCU20rSzQ3ajVTUFNkaytiVGh3UDZDZDhzWGUKNmljS3YvMXI0SjVqd1pFTmRna\ -2hKeHVuMkE1WVA0c0NBU21JSFFXWmU1cGdFQ0ljUXBHZjNBVllVdVpGeXZ6cApLL3VBRTc0L3NMK0VU\ -UnhDUlhkN0ZJM2cwZFVySTA2WUFIV1FkY1JpdWpIUU12ZXB3V2RrdUdkaE9wV3VRTy9oCjc0MGgvRlZ\ -0dFdOMjc3ajhhc3ptSDA3T2lRWFIwTU1mN2FEaldMaElMYkd2YkV4TzN0TnRBYXZ6cGxmMUNSNk8KUm\ -1nNDdhVTZpR3lON1MwTE85RUJBb0lCQVFEWWMwelh2UmJHTWxkVVZnOFFTT1ZHKzV4NXVocTVyQ2xtQ\ -lp2ZApvYitWS1hkMXBONThraWkxTFBiYUJmZG9JcWVUamdOK0E0YnVpT3RGNjdCWUdISkp0WTk4ZWR5\ -RkpZREtCSzIyCmthRml2eWVuV01UWG5jaHMvUmNXMnVGS1M3cFAyNDRXYkF2clQxZGxmTTF3L1JzbTZ\ -QVjF1dndPSmNJcEdLSzgKWWgyN2hiaThUbEF6bnJCSS9TbGZwNlpxV3Y0MFRLanNNdmVGVFNWRU5yd1\ -F2MVl4TjcxbjA1UWRVUkIrT0lJSQpPenpNMWpNcm43cjNmS3RKTEx0RllEdUhJZzJDL2E0cUZORmpjM\ -296V1laQkhlcDkrWlEyOTgxdy96VTJIZ0oxCkxiajZjMy9qQU5EakI3MnRkcWMxVUkvMWhwVUh2WDJz\ -emY2czM0L3VSY3RlaFgxWEFvSUJBUURaazVaT01RdVMKNzlVQkhCeTRFbFR2cVkvUUxjMUd1aG9LVXE\ -0dzhONEtJZlpMSDk5TTcwek9JWjNoaEdaYWJDREZHN3RqSDl4Vwo3UnhXVWt5cFRDK1h5TTc1YjF0YW\ -9jRVVyZ1hnUnE4R1JFRXg3OUtwWUE3ei95TVc1OHJCTWhHTWlGZ1JTM00zCmNQSzg2dHBvaTFDN2crd\ -lUxcjJwcDEzN0habzZiUHpKTFRPNXA1OG1tTFRPQVpKd1VSZ3pzZWJnc1dsWXZlR0wKZWNBZ3lYbUtt\ -WmVSeGRNUFlCK0NmMno1TWZ5Ujk4MHRVSDJPaitGQjlJNExzRTExREphZ2wwS1lHMWxKRTBFagpodEV\ -1cTFOTG9DcVkzYXVKYjhtRUpqNlA2Ylc2SytCZTBlUi9CS1MraGxxb3VOYWEySnlhSDc4Qis2U1VZWE\ -RuCnd6MityWUhVZEI4M0FvSUJBRFlXc2ZnaloyS0Z4KzdxUm45aVIvRXlCUXNpSjNXSWdSdmVnUEdrY\ -nRTZWRSeXYKNDIwcnRRSjVSd0o2aFRXL216S3pSVW9qSlgvTU5VYld1ODEzNW05bThJRkJqb3F6TVhq\ -S0xJSzM1NlZlY1ZGUApUSGs1RTVHd3VTbGI3dnA2N0FieXJaSUswL3VzYXdHUWEySTF6YWd1aE5BenR\ -yTHVXcE9jZFdZditwQVd2WEJJCi9aKzRvd0xLU0tGL3FvVmZVYkRPQzFSaTlCbWFpcHArTndiVVdYeV\ -pHanFzMDVGejVYUTFPTUZIMUV5M3BqZmIKaFlRODRpeTZBZDQzU3dqY3lKV1lRUUtCQzBZWDRFeWVyW\ -Dd1TTkvaEUxbWRHUGlJdmNwVk8zWCt3Ly9LQndZNQorUGtTd1NKc3lTSDRqTkRsSGE2K2VuNUpSNy81\ -YWVVNENiY0lFcWNDZ2dFQkFLelMvNnhDWnZnalN5V2poK2hxCm4wN3plQW1icUJmTEVZNHJtTFBGVUF\ -ucWFqSElNbDV4SXFnRnFkd05pQ1BCQ2RLbnNaUU9KYjVpZjRUTndKa2wKckJRNzdMUFRVVlJQY2dnVU\ -p4Uzc4S0Rnckl5Vys5V1FPTEIxZEJEb3MzUDhhbFlmb3h5eHV1Wko4SFpCY3BWaQpQQkdHdTFnSDd3V\ -0lyUzBmbVhkWlJQNGp5cGRvM3hFUWNXWEZkK1dCZE9EektmcEcwZkFzZTdDSFdDWnpBdmttCkFYQklH\ -OXQxdGZHNWQvMEZTS05GbTVPb0FPT3h3L0xZNTgrL0RmZXd0U0VBcFdRZkxTL1BmSWxVdUdvQ3FwcEM\ -Kc2pOVXVNSGxxc011Z2JsY29mNHNoZityWjMzQldYOEJSNWdIb21mRE1ibDNDQWp5TXd1dHpybzVxcD\ -BBUTBWWApxOGNDZ2dFQkFLakFXVVRYY1F2TE8yYkxOZmJBTUlSVXk1T2lTZmJCbDNYRThSZnNzaUt2V\ -VBnTFcwSlV1V3FLCjdGdUFxTlJPRHhrS0pMSTdyQlo2YVNqNitFWHpUMnJwY2dFWktnSjFOUEFRdFNs\ -UjNJUkcyU3JJdjBFK3UzbkUKK1laa3pOa2Q0MUJqTkRjRm1HV21lZk5ROUJmaVIrZlZFSkZmcE5oSkl\ -mNUloSWU0RUtZUE5VUXNua0tSVTlxUApzWi9idXBXc2w4bWVFcko3bllJQ05ucHpnSHRpNXdSMlliVF\ -VXT01odmRFUldxMnhTV3BBYmtNMElhZDBUc05kCmUrYVRQVmJOMXFibFZLMm1qUTl2YS9JSkVuSE51V\ -E9TREtJeUpvcVArQkxiRTVjQU5acXQ2OFFadWdOc2RxNHkKV2FoeStydU5LS1F3Mk5MYzQzZUtsNmxv\ -bXdtRlFZOD0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=") - ("TLS.password" . "") - ("TLS.negotiationTimeoutSec" . "-1") - ("TLS.method" . "Automatic") - ("TLS.ciphers" . "") - ("TLS.certificateFile" . "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZHVENDQ\ -XdHZ0F3SUJBZ0lJU1pUdlZPQnh3akF3RFFZSktvWklodmNOQVFFTUJRQXdTVEVOTUFzR0ExVUUKQXhN\ -RVNtRnRhVEU0TURZR0NnbVNKb21UOGl4a0FRRVRLR1l6TXpRMVpqSTNOelZrWkdabE1EZGhOR0l3WkR\ -rMQpaR0ZsWVRFeE1XUXhOV1ppWXpFeE9Ua3dIaGNOTWpFd05ERTJNVGN6TWpFd1doY05NekV3TkRFME\ -1UY3pNakV3CldqQlFNUlF3RWdZRFZRUURFd3RLWVcxcElHUmxkbWxqWlRFNE1EWUdDZ21TSm9tVDhpe\ -GtBUUVUS0dFM09XTmwKTURVNE16VmhPRFV5WlRsbU5qWmxOelF3TURjeU5EUXdPVFZpTVdaa1kyVXdO\ -bVV3Z2dJaU1BMEdDU3FHU0liMwpEUUVCQVFVQUE0SUNEd0F3Z2dJS0FvSUNBUUMzOW9WdHFzbVBnWkl\ -IcHpTVGtlT3BlWC9CSEx2KzFTYnJPSFpVCkRxMDRWQlFOQTZiZkhTUlkyaWxxNVhFYXlzVUpsM0JrLz\ -NLcWRIUnFEV01wQ1dpcE1Vc2FwSGxJR0tSWHEwbXgKUGdvVjg2UlFQbm9XQkU3YVlVRGU5SWV5cTJpZ\ -WV6QytZWEpwbVk5Y05tblpaMFlHOHJGMEVpWFA0SHpVWGphZgpJUys3Sk0yeWU2clJaSDVwb3RwUDZl\ -eDV4alVFbjRBRXVoblhielNBNnZMaC9wYnhIUVBHMVlmNHFhcU9TY1lXCldGVmhOcklQQlRSTWdkWk1\ -kRkU4dFlRcmlDZDU1eHVIa2J3K0JmNVVmVENqN25tVEcybDJNbGcrSXBHVkFXUFQKVjZJdDRTYkt1Wn\ -VuTGZkQ0tha0lNN0pwN1dnSVo2TUR3ZGhRUFoyTVJPQzN0b0c1ajh4aFZOU0Q5QlZiOGZBcgp0Znh3Z\ -XZZYkJNcjhHUVdiUGJUSmJPbWR2Z1BkRnFzU29ReEJ0VHFkSG1TMG53RjJ4UGZrdm1Cd3BSWWhHNHpu\ -CmZVbTJQUHAwSCtYOUpPendEdUFiOStHNHFmbSt2emw3dTdKN2pydE1HQlJlSlREdGFNd2RHa0lmcUN\ -RZGREZHEKaTNYc1R1VTlTbm1TeDZHcU9lemVNOG5OeXZrWVo0dlJPbzgxc1NSYzQ4L3pZZjJ1OFBJUm\ -4vVy9xZk9hbjVIbgpHMXhTQVA2ZHBwM0NWNGE4MFBXbmcrM3F0ODVuR2tpNjZNNDVJZ0Y4NHZwRGUyM\ -2YyZnprTk5OOTk1VTBvUStKClRoOXhtR2liZnp5Qml5OXd0VFR1WFgwd2N4ME5vcDY3WGhRQ1VQRkhy\ -bVd6WjV5bXhEdkRhcUN1U3NwcG0vTkQKVUVkMnNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkRBVUFBNEl\ -DQVFCZmRCc0p4RVVWeFRBeG5vNll1bEFoR1ZvUAplWG8xMHIwOE5DcDZRZlJxeGJlTkZ5aXEvKzEwSE\ -NpL1RoZk41OVdLNlpudFF4NlFNZUxEUVZTb0NjNzlaaWQ4ClE2RUdsWkp1c2RTTmg0VjVteXRCQVZHZ\ -2J2aXJFWU1Wcm5jWWg3bHR2LzVuSGRsbyt2WXV3Vzh0aEhHTk1TUkIKQmhJN2xydmpqY3NkbVl6L1Bp\ -NFNZdkg3c3RaVWpRYmUzNzh3UE90b3lBMTNybEtQUTF4QjJFbUxISDYya21WTQowa25wL1hQQ2o2alR\ -zakpFWko1NzZNMmloYy9DTzkvREVlWWZ4RFlJb004NEF5dTc0UU9UUVN1cnVHRjF5T1RKCmlxRkltTH\ -hMcWRxNk1Zdmx1QjFmTzlKaU9QaS84UEJFTTVpeGYzajlGOFVMQTZUTU1NSklFR1ROeUNzOWpzQloKR\ -UNiWVgweTY4WWtGYnYzQmNWaGk2K1VVWVhBVUd0akhwQ3Z2VThYaHowL0RVZFVaTkpqejRJeWl1ZE5N\ -dnFjQgppSEJDdkxFS1B4VFd1ZlFZaGM5ZkVXTm1valc1WSsvalBVeDcrQWxPM1RXZm1OclBpWDJuSEd\ -5UG5tYjZIR2hWCmp3UU0wT1h4cGxnZ2dQMWpVZ0VYWWRucStjYzdRMy94RFIvdi9Fd244T1d4OVB3eV\ -Y0WFZwZXY5QnpmeWwyQTMKdFhWVkFKMzJtcTlPQmVadXczdWlWU2E2TWdCVG80eXcrYnBjU2VIU2xXO\ -XNRc1NYY3dUNjhvOUdhR2hMNjdXMApwQWx4R2VxSWRtbTZ0MkxIUCtQQnBtL3BweTMvTmh1NFIwMTNv\ -Znp5V2Y0bEcrVnpWVGE0eXVqU1J3UlluYmZyCjlSWER0L0I5VEg5TnNsamdBQT09Ci0tLS0tRU5EIEN\ -FUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ0F3SU\ -JBZ0lJRm1tNmZuaWRndEl3RFFZSktvWklodmNOQVFFTUJRQXdUREVRTUE0R0ExVUUKQXhNSFNtRnRhU\ -0JEUVRFNE1EWUdDZ21TSm9tVDhpeGtBUUVUS0RjNVpqSTJaVFZpWTJNeU9EWXlPREppT0dFMwpPRFF6\ -TUdOak1EWXpNakV4T1RFNFkyWm1PVGd3SGhjTk1qRXdOREUyTVRjek1qRXdXaGNOTXpFd05ERTBNVGN\ -6Ck1qRXdXakJKTVEwd0N3WURWUVFERXdSS1lXMXBNVGd3TmdZS0NaSW1pWlB5TEdRQkFSTW9aak16Tk\ -RWbU1qYzMKTldSa1ptVXdOMkUwWWpCa09UVmtZV1ZoTVRFeFpERTFabUpqTVRFNU9UQ0NBaUl3RFFZS\ -ktvWklodmNOQVFFQgpCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFQY0czaUQwUk84NE5XL2ZGVllWNHBLQ2My\ -ZE1EQXFCZVN2NEpCcTQ3OHQ5ClhjeERPeEhqSnYvR0dLRnFoVyt4cWFDOUJKZG44TGthdjBBTnMvV1d\ -3YXRNSDVsYlQrZlc0T2pGOVNCZ1Ivc2UKUlNPdWE1QncrSVJlSk5VSnR3enBJS3k1S1RhMnEwd0RyUX\ -hObXBuci9UK2NEK2RQSTJveUlFd2xwRHNKT2lVMAp6eStDU2pBb0FTYkh5TWcraGQwcWJiRFBUck1uR\ -2xKYkZubXdBQTBaRWU3aWg5Ymx0dlFPaU9TQWJVQm9IUHFpCmNtQW41RVFoNjVFa3dCb1p1U3hqanlC\ -VzZpWUJUbmc5S2c4U2pPVk9BYkJqaXZFZ3RyaFpYZ1JVdE11UWFmaDcKcjRRWDRuYjhRTUE5WXlhME8\ -xbFBJTUd5VEtiNHFIaE02VTd1RHV0NUFjcTN1NGFaZElPQ2prbUNFM3l4UU5UWAo5Vzl0b1NvV1l2YV\ -I1d1gxaFI5eVZCMFZ4V0N0djdZZklha1JiaWkxMS8xZHp3M1dXU1dMbTArTU4rY0pUdnluCk1EU29GN\ -jRpRmcycTVYd1VtWXpPRmJKQUtVd3M4QmVhcFRCVnRYaldDZmNKM01xZEZScDZvM25Lenlja0YwamoK\ -UjdLSmNRaWhJUW1sbXM1WEtzeHV1d0VMMmFKQ0o0Qm9uSUJDaXZWa09LbllsamtDeVNObzkzamlMalJ\ -4M0ZtRwpoTnJyT3lNbjFIcEd0Z1J2eFUxOC9Ob3FFaWtpdEhCeUtYNW5XU1lUUWN4MXlZY3kxcVo5cj\ -RqTmdQRzNuUHg0CjJEN2tPTlJPWXNVYXNBUlVTZHJqdlc5aXB2TDAvN2s1Yi9sRzh1WThPODN0NUFPS\ -TFVUG85Y05qMzNyeWJwbUgKQWdNQkFBR2pRekJCTUIwR0ExVWREZ1FXQkJUek5GOG5kZDMrQjZTdzJW\ -MnVvUkhSWDd3Um1UQVBCZ05WSFJNQgpBZjhFQlRBREFRSC9NQThHQTFVZER3RUIvd1FGQXdNSEJnQXd\ -EUVlKS29aSWh2Y05BUUVNQlFBRGdnSUJBRDMrCjlscFluMjZyeG5pekY2UkNvZFFFVmgvOVF2RWp1V3\ -dHUWZqa3gxb3VlVjdDMzUyWnpIT2hWU3VGcG43TUxkVFcKamI2dWhMRkpoMWtlTDlYZ0pHalMyV0Uwd\ -DlJUGp2UWx5UHIwRWJoWGRJNDJMYUR0NDk0dWQ2MEE0bWg0bW1zbwovcjY2NERKOWMwUjZBOUdQZUJj\ -Wi9zekhMQjJ4VmM5M3hYNjVjcmNoVTFLMDBVK2ZZUWtHS0xidFFXeUZzdnlKCmRHdFRxamVlYVVNeGt\ -5dFFWNWFxVHJ1SG4vV3U2RWRZejYrZ0ViQXJHUURRK1J3QmMxMDNGcWRIU0xReWdEWHoKd0pNVGhDRE\ -dBVmR6V2NCemJYL1JIZms5bTZzK01HblNJUFBrNG9FOUdFbGdQc2JUZ2FrTzc3aElUdzRxZXJiTAp2M\ -0tiaTVOeVZZVFNVa3hDb0VRSkIrWlZJT3BVNFhRdVAvNkZkWldBRk1LU0Jkd1JLcnRmT1hZT1ExcVBv\ -YU1uCjZPR1VGMU0rYWZ0dTNCMkNhZ2ZMaE5hbVBoSjdxWTNSMzJhK2VRVllvYWlESXZKMElIUHFnQ0N\ -uWXlIaVBHTjQKS3VvaE9IZFpXTkxOUXdIQTg3SDV5NEwrSGZISmdFZFppdWYrbTNMa3JJcnN5MWFoUj\ -gyaEpZVkhreHVyZVRDcQpIR2NJaUIvTHpkSG91WFVrWGNrNjRvWXVRQnM3ZE9KVEx6bGlibU8xelRzV\ -WRoc0d4dE9zc2lYWkFjUURmZHBnCjNHdFJ3UkRwd0ZtL2k5TC9UWjMzMjBwY0VZd21aN1dBSzJRa3Z6\ -eWZlelRGeXdLWmh3c0RQRkwycllzLzhXdWsKRk1sek5BVmRMTytTS1RxeGlkM0l1elFaQ0FwU05ybTJ\ -MY2ZVN2grWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS\ -0tLQpNSUlGWFRDQ0EwV2dBd0lCQWdJSXV0T3MxUXBsSVRvd0RRWUpLb1pJaHZjTkFRRU1CUUF3VERFU\ -U1BNEdBMVVFCkF4TUhTbUZ0YVNCRFFURTRNRFlHQ2dtU0pvbVQ4aXhrQVFFVEtEYzVaakkyWlRWaVky\ -TXlPRFl5T0RKaU9HRTMKT0RRek1HTmpNRFl6TWpFeE9URTRZMlptT1Rnd0hoY05NakV3TkRFMk1UY3p\ -NakExV2hjTk16RXdOREUwTVRjegpNakExV2pCTU1SQXdEZ1lEVlFRREV3ZEtZVzFwSUVOQk1UZ3dOZ1\ -lLQ1pJbWlaUHlMR1FCQVJNb056bG1NalpsCk5XSmpZekk0TmpJNE1tSTRZVGM0TkRNd1kyTXdOak15T\ -VRFNU1UaGpabVk1T0RDQ0FpSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0lCQUxT\ -cGduY2tYODd3OE5vVzdRWE5DbWtHSHphVnN1NE9UbjFSSUhqVQpGR0w5MjNvVlk5RHVoZjFtVnllamg\ -remVCOCszaTR4b1E5MU04WDdTVnk4SHZxejQ4c0I2djAxMk5RTGpwN08xCkd4dmc1a0REeDRqbmVyUW\ -d6d0JzVEFuaC8xNy9SOEFDZkhINzYzUjhMRjY1clJPSStOT21KNUV2cU9rRWJJOXYKb0UySko1czBVS\ -HZZM1hTQ2pid01ROXRkM2c0SG9LNmRYSU44a2d6ejg0Yi83YkF5TnZ4a09rNnVSei9CSjhhZQozWjZu\ -dElza2tmVHhIZFpBbGtpVW5DU21Bb3VwQ01TbGRLS3RMRzNja2FhN0FDMDRxS29Rekc0WTN2TlRCOUl\ -mCmdvYThOazlKMThoaFN2REVuR0haaURLR1dkQVA5RW5GRHZERnRFRFZyVVZSWUN5WHdyejMya29Na3\ -dYODVUMTgKc1F2TUZSdWRsa1BoOGxmbnFEWTFpZEd1R2IvSGlnU1ZqalJKZlVTbS9lZ3hycStSTnpJW\ -GFBOWNuOXAxQkdFbwpZU0YxeUdtTEgzSkJscUhVRTg4VnFkenZRdXZ3Mkhvakdvd0pkdlhmU2loZk5W\ -OTh6VkEzQ2o3L0d3V2w0VU9OCkRUVUk2SzUrR3Y0a01iYXdxcWduaFlHbHBlZVp2bHE2UlI3OVpNblR\ -WVjJ1RWo4aERYQzdFTGN2Yk5tekJSSkoKS0gzaUxIbmNlSkxOV2NIMUV0K0pkaEFRR2xEYmk2RVR0dH\ -RuaWVaN3ZkMyt6UE00eXpkb3JEMlJXUjRyWG1acQpGRlI5eTBab2hQVTBjVmdKZHFtdUpyV01ZeTVsO\ -Gt5SWdwaExkdTR1WWRpc0RPMG1ianEySE55K3pEakpyYXFHClNWTkZBZ01CQUFHalF6QkJNQjBHQTFV\ -ZERnUVdCQlI1OG01YnpDaGlncmluaERETUJqSVJrWXovbURBUEJnTlYKSFJNQkFmOEVCVEFEQVFIL01\ -BOEdBMVVkRHdFQi93UUZBd01IQmdBd0RRWUpLb1pJaHZjTkFRRU1CUUFEZ2dJQgpBRWFDblluYm9yWm\ -RhWWZRUmxSb0dtSE94T1g5VFdNMXY0dWEweCtFcG11RDVWRDVlWEY1QVZkMlZadEdaeHYvCkd2VnFBU\ -2l0UTk3ampKV2p2bURWTUZtb3hQSmRYWDFkYTd5cmJYeFRmYm1mM1pac1RpdmZVdWQxYThxdUN3dTUK\ -TnBrdHFjV0JyMHRnNzFhOXlidHJOdm1GczZGRE1WMXkrY2JxYlp1UWlDWnc0WmZhekFaeTRQRGNocE9\ -SNjRCSwpBWFZIT05HcWFoV2hwcDkwd2E0TFEwUTE1U3FDR25kYVI4SHg5MHJOeEdkRjM1T1BLUjd1TD\ -drWDU4ZGxaWStDClJJK1pKMndYMzJUZzgrc1RtTmNaUTliWDdvS0t3R3E5Uk94SjZJSUhOSnN2bXhaS\ -nlPcmE2N21hNTd3OWxiQnUKSzJlQ3cwZjRDeHdLNU1LNStkbWx6R3dhZmJlMG00TTBvVjlhWUxZTzAw\ -R3Iza05heW9PdjRRVGtEM2pCMzVSMQpDMGJnQmk2eU1sTVJ2akZ5eEZkOFJpL003VG1jcXNObTB5akl\ -qbkZaenVtMFZTR1NLMXlRU3Flak40S0Y2R0JMCllpZ2JpM3c4WG5HYm9pZGdBUE9ncVVJeTJ1dEU1Ml\ -lzVXFsVHVncXhtM2xDOUhzaDM2UFJLNURDUG93eHVUNlgKcXo1M1ZiN2h6TkxLelpiRlJzbUdFOFY2c\ -WM2bXZTbUFXa25nL3QwaStXVmdGVkZuZFQrQ0oyNTJsa0ZacGljdAp6ekdETW44VUNDRUp4TDRKTklT\ -M2lLOUhlRys2MlZuay9QOEM3YVpLSXpVdjFud25rcVdUUUFYWDBKckJGdDdICjI5ZDk1RElmRTRuT0F\ -yS0JFNHc2Z1R4SU1uZzVzWi9ZbDFjcG5wUHlsR3VICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K") - ("STUN.server" . "") - ("STUN.enable" . "false") - ("SRTP.rtpFallback" . "false") - ("SRTP.keyExchange" . "sdes") - ("SRTP.enable" . "true") - ("RingNS.uri" . "") - ("RingNS.account" . "0790738ce15fa05933b49dd77034312787da86c3") - ("DHT.PublicInCalls" . "true") - ("Account.videoPortMin" . "49152") - ("Account.videoPortMax" . "65534") - ("Account.videoEnabled" . "true") - ("Account.username" . "f3345f2775ddfe07a4b0d95daea111d15fbc1199") - ("Account.useragent" . "") - ("Account.upnpEnabled" . "true") - ("Account.type" . "RING") - ("Account.ringtoneEnabled" . "true") - ("Account.rendezVous" . "true") - ("Account.publishedSameAsLocal" . "true") - ("Account.publishedPort" . "5060") - ("Account.publishedAddress" . "") - ("Account.presenceSubscribeSupported" . "true") - ("Account.peerDiscovery" . "false") - ("Account.managerUsername" . "") - ("Account.managerUri" . "") - ("Account.mailbox" . "") - ("Account.localModeratorsEnabled" . "true") - ("Account.localInterface" . "default") - ("Account.hostname" . "bootstrap.jami.net") - ("Account.hasCustomUserAgent" . "false") - ("Account.enable" . "true") - ("Account.dtmfType" . "overrtp") - ("Account.displayName" . "dummy") - ("Account.defaultModerators" . "") - ("Account.audioPortMin" . "16384") - ("Account.audioPortMax" . "32766") - ("Account.archiveHasPassword" . "false") - ("Account.allowCertFromTrusted" . "true") - ("Account.allowCertFromHistory" . "true") - ("Account.allowCertFromContact" . "true") - ("Account.allModeratorEnabled" . "true") - ("Account.alias" . "dummy") - ("Account.activeCallLimit" . "-1") - ("Account.accountPublish" . "false") - ("Account.accountDiscovery" . "false"))) diff --git a/gnu/tests/telephony.scm b/gnu/tests/telephony.scm deleted file mode 100644 index 1155a9dbc2..0000000000 --- a/gnu/tests/telephony.scm +++ /dev/null @@ -1,366 +0,0 @@ -;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2021 Maxim Cournoyer . -;;; -;;; This file is part of GNU Guix. -;;; -;;; GNU Guix is free software; you can redistribute it and/or modify it -;;; under the terms of the GNU General Public License as published by -;;; the Free Software Foundation; either version 3 of the License, or (at -;;; your option) any later version. -;;; -;;; GNU Guix 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 General Public License for more details. -;;; -;;; You should have received a copy of the GNU General Public License -;;; along with GNU Guix. If not, see . - -(define-module (gnu tests telephony) - #:use-module (gnu) - #:use-module (gnu packages) - #:use-module (gnu packages guile) - #:use-module (gnu tests) - #:use-module (gnu system vm) - #:use-module (gnu services) - #:use-module (gnu services dbus) - #:use-module (gnu services networking) - #:use-module (gnu services ssh) - #:use-module (gnu services telephony) - #:use-module (guix gexp) - #:use-module (guix modules) - #:export (%test-jami - %test-jami-provisioning)) - -;;; -;;; Jami daemon. -;;; - -(include "data/jami-dummy-account.dat") ;defines %jami-account-content-sexp - -(define %dummy-jami-account-archive - ;; A Jami account archive is a gzipped JSON file. - (computed-file - "dummy-jami-account.gz" - (with-extensions (list guile-json-4 guile-zlib) - #~(begin - (use-modules (json) (zlib)) - (let ((port (open-output-file #$output))) - (call-with-gzip-output-port port - (lambda (port) - (scm->json '#$%jami-account-content-sexp port)))))))) - -(define %allowed-contacts '("1dbcb0f5f37324228235564b79f2b9737e9a008f" - "2dbcb0f5f37324228235564b79f2b9737e9a008f")) - -(define %moderators '("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) - -(define %dummy-jami-account (jami-account - (archive %dummy-jami-account-archive) - (allowed-contacts %allowed-contacts) - (moderators %moderators) - (rendezvous-point? #t) - (peer-discovery? #f) - (bootstrap-hostnames '("bootstrap.me" - "fallback.another.host")) - (name-server-uri "https://my.name.server"))) - -(define* (make-jami-os #:key provisioning?) - (operating-system - (host-name "jami") - (timezone "America/Montreal") - (locale "en_US.UTF-8") - - (bootloader (bootloader-configuration - (bootloader grub-bootloader) - (target "/dev/sdX"))) - (file-systems (cons (file-system - (device (file-system-label "my-root")) - (mount-point "/") - (type "ext4")) - %base-file-systems)) - (firmware '()) - - (services (cons* (service jami-service-type - (if provisioning? - (jami-configuration - (debug? #t) - (accounts (list %dummy-jami-account))) - (jami-configuration - (debug? #t)))) - (service dbus-root-service-type) - ;; The following services/packages are added for - ;; debugging purposes. - (service dhcp-client-service-type) - (service openssh-service-type - (openssh-configuration - (permit-root-login #t) - (allow-empty-passwords? #t))) - %base-services)) - (packages (cons* (specification->package "recutils") - (specification->package "strace") - %base-packages)))) - -(define %jami-os - (make-jami-os)) - -(define %jami-os-provisioning - (make-jami-os #:provisioning? #t)) - -(define* (run-jami-test #:key provisioning?) - "Run tests in %JAMI-OS. When PROVISIONING? is true, test the -accounts provisioning feature of the service." - (define os (marionette-operating-system - (if provisioning? - %jami-os-provisioning - %jami-os) - #:imported-modules '((gnu services herd) - (guix combinators)))) - (define vm (virtual-machine - (operating-system os) - (memory-size 512))) - - (define username (assoc-ref %jami-account-content-sexp - "Account.username")) - - (define test - (with-imported-modules (source-module-closure - '((gnu build marionette) - (gnu build jami-service))) - #~(begin - (use-modules (rnrs base) - (srfi srfi-11) - (srfi srfi-64) - (gnu build marionette) - (gnu build jami-service)) - - (define marionette - (make-marionette (list #$vm))) - - (mkdir #$output) - (chdir #$output) - - (test-begin "jami") - - (test-assert "service is running" - (marionette-eval - '(begin - (use-modules (gnu services herd)) - (match (start-service 'jami) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) (number? pid)))))) - marionette)) - - (test-assert "service can be stopped" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base)) - (setenv "PATH" "/run/current-system/profile/bin") - (let ((pid (match (start-service 'jami) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid)))))) - - (assert (number? pid)) - - (match (stop-service 'jami) - (services ;a list of service symbols - (member 'jami services))) - ;; Sometimes, the process still appear in pgrep, even - ;; though we are using waitpid after sending it SIGTERM - ;; in the service; use retries. - (with-retries 20 1 - (not (zero? (status:exit-val - (system* "pgrep" "dring"))))))) - marionette)) - - (test-assert "service can be restarted" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base)) - ;; Start and retrieve the current PID. - (define pid (match (start-service 'jami) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - (assert (number? pid)) - - ;; Restart the service. - (restart-service 'jami) - - (define new-pid (match (start-service 'jami) - (#f #f) - (('service response-parts ...) - (match (assq-ref response-parts 'running) - ((pid) pid))))) - (assert (number? new-pid)) - - (not (eq? pid new-pid))) - marionette)) - - (unless #$provisioning? (test-skip 1)) - (test-assert "jami accounts provisioning, account present" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base)) - ;; Accounts take some time to appear after being added. - (with-retries 20 1 - (with-shepherd-action 'jami ('list-accounts) results - (let ((account (assoc-ref (car results) #$username))) - (assert (string=? #$username - (assoc-ref account - "Account.username"))))))) - marionette)) - - (unless #$provisioning? (test-skip 1)) - (test-assert "jami accounts provisioning, allowed-contacts" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base) - (srfi srfi-1)) - - ;; Public mode is disabled. - (with-shepherd-action 'jami ('list-account-details) - results - (let ((account (assoc-ref (car results) #$username))) - (assert (string=? "false" - (assoc-ref account - "DHT.PublicInCalls"))))) - - ;; Allowed contacts match those declared in the configuration. - (with-shepherd-action 'jami ('list-contacts) results - (let ((contacts (assoc-ref (car results) #$username))) - (assert (lset= string-ci=? contacts '#$%allowed-contacts))))) - marionette)) - - (unless #$provisioning? (test-skip 1)) - (test-assert "jami accounts provisioning, moderators" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base) - (srfi srfi-1)) - - ;; Moderators match those declared in the configuration. - (with-shepherd-action 'jami ('list-moderators) results - (let ((moderators (assoc-ref (car results) #$username))) - (assert (lset= string-ci=? moderators '#$%moderators)))) - - ;; Moderators can be added via the Shepherd action. - (with-shepherd-action 'jami - ('add-moderator "cccccccccccccccccccccccccccccccccccccccc" - #$username) results - (let ((moderators (car results))) - (assert (lset= string-ci=? moderators - (cons "cccccccccccccccccccccccccccccccccccccccc" - '#$%moderators)))))) - marionette)) - - (unless #$provisioning? (test-skip 1)) - (test-assert "jami service actions, ban/unban contacts" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base) - (srfi srfi-1)) - - ;; Globally ban a contact. - (with-shepherd-action 'jami - ('ban-contact "1dbcb0f5f37324228235564b79f2b9737e9a008f") _ - (with-shepherd-action 'jami ('list-banned-contacts) results - (every (match-lambda - ((username . banned-contacts) - (member "1dbcb0f5f37324228235564b79f2b9737e9a008f" - banned-contacts))) - (car results)))) - - ;; Ban a contact for a single account. - (with-shepherd-action 'jami - ('ban-contact "dddddddddddddddddddddddddddddddddddddddd" - #$username) _ - (with-shepherd-action 'jami ('list-banned-contacts) results - (every (match-lambda - ((username . banned-contacts) - (let ((found? (member "dddddddddddddddddddddddddddddddddddddddd" - banned-contacts))) - (if (string=? #$username username) - found? - (not found?))))) - (car results))))) - marionette)) - - (unless #$provisioning? (test-skip 1)) - (test-assert "jami service actions, enable/disable accounts" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base)) - - (with-shepherd-action 'jami - ('disable-account #$username) _ - (with-shepherd-action 'jami ('list-accounts) results - (let ((account (assoc-ref (car results) #$username))) - (assert (string= "false" - (assoc-ref account "Account.enable")))))) - - (with-shepherd-action 'jami - ('enable-account #$username) _ - (with-shepherd-action 'jami ('list-accounts) results - (let ((account (assoc-ref (car results) #$username))) - (assert (string= "true" - (assoc-ref account "Account.enable"))))))) - marionette)) - - (unless #$provisioning? (test-skip 1)) - (test-assert "jami account parameters" - (marionette-eval - '(begin - (use-modules (gnu services herd) - (rnrs base) - (srfi srfi-1)) - - (with-shepherd-action 'jami ('list-account-details) results - (let ((account-details (assoc-ref (car results) - #$username))) - (assert (lset<= - equal? - '(("Account.hostname" . - "bootstrap.me;fallback.another.host") - ("Account.peerDiscovery" . "false") - ("Account.rendezVous" . "true") - ("RingNS.uri" . "https://my.name.server")) - account-details))))) - marionette)) - - (test-end) - (exit (= (test-runner-fail-count (test-runner-current)) 0))))) - - (gexp->derivation (if provisioning? - "jami-provisioning-test" - "jami-test") - test)) - -(define %test-jami - (system-test - (name "jami") - (description "Basic tests for the jami service.") - (value (run-jami-test)))) - -(define %test-jami-provisioning - (system-test - (name "jami-provisioning") - (description "Provisioning test for the jami service.") - (value (run-jami-test #:provisioning? #t)))) - -;; Local Variables: -;; eval: (put 'with-retries 'scheme-indent-function 2) -;; End: diff --git a/tests/services/telephony.scm b/tests/services/telephony.scm deleted file mode 100644 index b4a0f120d4..0000000000 --- a/tests/services/telephony.scm +++ /dev/null @@ -1,446 +0,0 @@ -;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2021 Maxim Cournoyer -;;; -;;; This file is part of GNU Guix. -;;; -;;; GNU Guix is free software; you can redistribute it and/or modify it -;;; under the terms of the GNU General Public License as published by -;;; the Free Software Foundation; either version 3 of the License, or (at -;;; your option) any later version. -;;; -;;; GNU Guix 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 General Public License for more details. -;;; -;;; You should have received a copy of the GNU General Public License -;;; along with GNU Guix. If not, see . - -(define-module (tests services telephony) - #:use-module (gnu build jami-service) - #:use-module (gnu services telephony) - #:use-module (srfi srfi-64)) - -;;; Tests for the (gnu services telephony) and related modules. - -(test-begin "jami-service") - -(define parse-dbus-reply - (@@ (gnu build jami-service) parse-dbus-reply)) - -(define parse-account-ids - (@@ (gnu build jami-service) parse-account-ids)) - -(define parse-account-details - (@@ (gnu build jami-service) parse-account-details)) - -(define parse-contacts - (@@ (gnu build jami-service) parse-contacts)) - -(define jami-account->alist - (@@ (gnu services telephony) jami-account->alist)) - -;; $ dbus-send --print-reply --dest="cx.ring.Ring" \ -;; "/cx/ring/Ring/ConfigurationManager" \ -;; "cx.ring.Ring.ConfigurationManager.getAccountList" -(define getAccountList-reply "\ -method return time=1622217253.386711 sender=:1.7 -> destination=:1.14 serial=140 reply_serial=2 - array [ - string \"addf37fbb558d6a0\" - string \"d5cbeb7d08c98a65\" - string \"398af0c6b74ce101\" - ] -") - -(test-equal "parse-account-ids" - '("addf37fbb558d6a0" "d5cbeb7d08c98a65" "398af0c6b74ce101") - (parse-account-ids getAccountList-reply)) - -;; $ dbus-send --print-reply --dest="cx.ring.Ring" \ -;; "/cx/ring/Ring/ConfigurationManager" \ -;; "cx.ring.Ring.ConfigurationManager.getAccountDetails" \ -;; 'string:398af0c6b74ce101' -(define getAccountDetails-reply "\ -method return time=1622254991.789588 sender=:1.7 -> destination=:1.19 serial=145 reply_serial=2 - array [ - dict entry( - string \"Account.accountDiscovery\" - string \"false\" - ) - dict entry( - string \"Account.accountPublish\" - string \"false\" - ) - dict entry( - string \"Account.activeCallLimit\" - string \"-1\" - ) - dict entry( - string \"Account.alias\" - string \"some-rendezvous-point-name\" - ) - dict entry( - string \"Account.allModeratorEnabled\" - string \"true\" - ) - dict entry( - string \"Account.allowCertFromContact\" - string \"true\" - ) - dict entry( - string \"Account.allowCertFromHistory\" - string \"true\" - ) - dict entry( - string \"Account.allowCertFromTrusted\" - string \"true\" - ) - dict entry( - string \"Account.archiveHasPassword\" - string \"false\" - ) - dict entry( - string \"Account.audioPortMax\" - string \"32766\" - ) - dict entry( - string \"Account.audioPortMin\" - string \"16384\" - ) - dict entry( - string \"Account.autoAnswer\" - string \"false\" - ) - dict entry( - string \"Account.defaultModerators\" - string \"\" - ) - dict entry( - string \"Account.deviceID\" - string \"94b4070fc7a8afa8482c777a9822c52e6af2e1bd\" - ) - dict entry( - string \"Account.deviceName\" - string \"some-device\" - ) - dict entry( - string \"Account.dhtProxyListUrl\" - string \"https://config.jami.net/proxyList\" - ) - dict entry( - string \"Account.displayName\" - string \"some-rendezvous-point-name\" - ) - dict entry( - string \"Account.dtmfType\" - string \"overrtp\" - ) - dict entry( - string \"Account.enable\" - string \"true\" - ) - dict entry( - string \"Account.hasCustomUserAgent\" - string \"false\" - ) - dict entry( - string \"Account.hostname\" - string \"bootstrap.jami.net\" - ) - dict entry( - string \"Account.localInterface\" - string \"default\" - ) - dict entry( - string \"Account.localModeratorsEnabled\" - string \"true\" - ) - dict entry( - string \"Account.mailbox\" - string \"\" - ) - dict entry( - string \"Account.managerUri\" - string \"\" - ) - dict entry( - string \"Account.managerUsername\" - string \"\" - ) - dict entry( - string \"Account.peerDiscovery\" - string \"false\" - ) - dict entry( - string \"Account.presenceSubscribeSupported\" - string \"true\" - ) - dict entry( - string \"Account.proxyEnabled\" - string \"false\" - ) - dict entry( - string \"Account.proxyPushToken\" - string \"\" - ) - dict entry( - string \"Account.proxyServer\" - string \"dhtproxy.jami.net:[80-95]\" - ) - dict entry( - string \"Account.publishedAddress\" - string \"\" - ) - dict entry( - string \"Account.publishedPort\" - string \"5060\" - ) - dict entry( - string \"Account.publishedSameAsLocal\" - string \"true\" - ) - dict entry( - string \"Account.rendezVous\" - string \"true\" - ) - dict entry( - string \"Account.ringtoneEnabled\" - string \"true\" - ) - dict entry( - string \"Account.ringtonePath\" - string \"/usr/share/ring/ringtones/default.opus\" - ) - dict entry( - string \"Account.type\" - string \"RING\" - ) - dict entry( - string \"Account.upnpEnabled\" - string \"true\" - ) - dict entry( - string \"Account.useragent\" - string \"\" - ) - dict entry( - string \"Account.username\" - string \"ccb8bbe2382343f7feb140710ab48aaf1b55634e\" - ) - dict entry( - string \"Account.videoEnabled\" - string \"true\" - ) - dict entry( - string \"Account.videoPortMax\" - string \"65534\" - ) - dict entry( - string \"Account.videoPortMin\" - string \"49152\" - ) - dict entry( - string \"DHT.PublicInCalls\" - string \"true\" - ) - dict entry( - string \"DHT.port\" - string \"7766\" - ) - dict entry( - string \"RingNS.account\" - string \"3989b55313a911b6f0c004748b49b254f35c9ef6\" - ) - dict entry( - string \"RingNS.uri\" - string \"\" - ) - dict entry( - string \"SRTP.enable\" - string \"true\" - ) - dict entry( - string \"SRTP.keyExchange\" - string \"sdes\" - ) - dict entry( - string \"SRTP.rtpFallback\" - string \"false\" - ) - dict entry( - string \"STUN.enable\" - string \"false\" - ) - dict entry( - string \"STUN.server\" - string \"\" - ) - dict entry( - string \"TLS.certificateFile\" - string \"/var/lib/jami/.local/share/jami/398af0c6b74ce101/ring_device.crt\" - ) - dict entry( - string \"TLS.certificateListFile\" - string \"\" - ) - dict entry( - string \"TLS.ciphers\" - string \"\" - ) - dict entry( - string \"TLS.method\" - string \"Automatic\" - ) - dict entry( - string \"TLS.negotiationTimeoutSec\" - string \"-1\" - ) - dict entry( - string \"TLS.password\" - string \"\" - ) - dict entry( - string \"TLS.privateKeyFile\" - string \"/var/lib/jami/.local/share/jami/398af0c6b74ce101/ring_device.key\" - ) - dict entry( - string \"TLS.requireClientCertificate\" - string \"true\" - ) - dict entry( - string \"TLS.serverName\" - string \"\" - ) - dict entry( - string \"TLS.verifyClient\" - string \"true\" - ) - dict entry( - string \"TLS.verifyServer\" - string \"true\" - ) - dict entry( - string \"TURN.enable\" - string \"true\" - ) - dict entry( - string \"TURN.password\" - string \"ring\" - ) - dict entry( - string \"TURN.realm\" - string \"ring\" - ) - dict entry( - string \"TURN.server\" - string \"turn.jami.net\" - ) - dict entry( - string \"TURN.username\" - string \"ring\" - ) - ] -") - -(test-equal "parse-account-details; username, alias and display name" - '("ccb8bbe2382343f7feb140710ab48aaf1b55634e" ;username - "some-rendezvous-point-name" ;alias - "some-rendezvous-point-name") ;displayName - (let ((account-details (parse-account-details getAccountDetails-reply))) - (list (assoc-ref account-details "Account.username") - (assoc-ref account-details "Account.alias") - (assoc-ref account-details "Account.displayName")))) - -(define getContacts-reply "\ -method return time=1627014042.752673 sender=:1.113 -> destination=:1.186 serial=220 reply_serial=2 - array [ - array [ - dict entry( - string \"added\" - string \"1578883327\" - ) - dict entry( - string \"confirmed\" - string \"true\" - ) - dict entry( - string \"id\" - string \"1c7d5a09464223442549fef172a3cf6f4de9b01c\" - ) - ] - array [ - dict entry( - string \"added\" - string \"1623107941\" - ) - dict entry( - string \"confirmed\" - string \"true\" - ) - dict entry( - string \"id\" - string \"5903c6c9ac5cb863c64e559add3d5d1c8c563449\" - ) - ] - array [ - dict entry( - string \"added\" - string \"1595996256\" - ) - dict entry( - string \"confirmed\" - string \"true\" - ) - dict entry( - string \"id\" - string \"ff2d72a548693214fb3a0f0f7a943b5e2bb9be03\" - ) - ] - ]") - -(test-equal "parse-account-contacts" - '((("added" . "1578883327") - ("confirmed" . "true") - ("id" . "1c7d5a09464223442549fef172a3cf6f4de9b01c")) - (("added" . "1623107941") - ("confirmed" . "true") - ("id" . "5903c6c9ac5cb863c64e559add3d5d1c8c563449")) - (("added" . "1595996256") - ("confirmed" . "true") - ("id" . "ff2d72a548693214fb3a0f0f7a943b5e2bb9be03"))) - (parse-contacts getContacts-reply)) - -(define getContacts-empty-reply "\ -method return time=1627400787.873988 sender=:1.1197 -> destination=:1.1463 serial=2127 reply_serial=2 - array [ - ]") - -(test-equal "parse-account-contacts, empty array" - '() - (parse-contacts getContacts-empty-reply)) - -(define %dummy-jami-account (jami-account - (archive "/tmp/dummy.gz"))) - -(define %dummy-jami-account-2 (jami-account - (archive "/tmp/dummy.gz") - (rendezvous-point? #t) - (peer-discovery? #f) - (bootstrap-hostnames '("bootstrap.me" - "fallback.another.host")) - (name-server-uri "https://my.name.server"))) - -(test-equal "jami-account->alist, no account detail value set" - '() - (jami-account->alist %dummy-jami-account)) - -(test-equal "jami-account->alist, with account detail values" - '(("Account.hostname" . "bootstrap.me;fallback.another.host") - ("Account.peerDiscovery" . "false") - ("Account.rendezVous" . "true") - ("RingNS.uri" . "https://my.name.server")) - (sort (jami-account->alist %dummy-jami-account-2) - (lambda (x y) - (string<=? (car x) (car y))))) - -(test-end) -- cgit v1.2.3 From 10f554700c417b84c22c56fdd007567a52c45d75 Mon Sep 17 00:00:00 2001 From: Maxim Cournoyer Date: Mon, 2 Aug 2021 16:06:04 -0400 Subject: Reinstate "services: Add a service for Jami." This reverts commit 4673f817938d9d2b1b40a072ab2e0c44a32ccc97, which reverted commit 69dcc24c9f0cdfea674eb690e7755d26a25ced2b with the fix detailed below. Thanks to Christopher Baines for reporting the failure and proposing a fix. * guix/self.scm (compiled-guix) [*system-test-modules*]: Add the test data files via the 'extra-files' argument. * gnu/local.mk (dist_patch_DATA): Move the tests/data/jami-dummy-account.dat file to... * gnu/local.mk (MODULES_NOT_COMPILED): ... here. --- Makefile.am | 1 + doc/guix.texi | 228 ++++++++++++ gnu/build/jami-service.scm | 587 +++++++++++++++++++++++++++++ gnu/local.mk | 5 +- gnu/services/telephony.scm | 684 +++++++++++++++++++++++++++++++++- gnu/tests/data/jami-dummy-account.dat | 392 +++++++++++++++++++ gnu/tests/telephony.scm | 366 ++++++++++++++++++ guix/self.scm | 2 + tests/services/telephony.scm | 446 ++++++++++++++++++++++ 9 files changed, 2708 insertions(+), 3 deletions(-) create mode 100644 gnu/build/jami-service.scm create mode 100644 gnu/tests/data/jami-dummy-account.dat create mode 100644 gnu/tests/telephony.scm create mode 100644 tests/services/telephony.scm (limited to 'gnu/build') diff --git a/Makefile.am b/Makefile.am index d5ec909213..5542aa1c56 100644 --- a/Makefile.am +++ b/Makefile.am @@ -489,6 +489,7 @@ SCM_TESTS = \ tests/services/file-sharing.scm \ tests/services/configuration.scm \ tests/services/linux.scm \ + tests/services/telephony.scm \ tests/sets.scm \ tests/size.scm \ tests/status.scm \ diff --git a/doc/guix.texi b/doc/guix.texi index 2298d512a1..9a9c85678c 100644 --- a/doc/guix.texi +++ b/doc/guix.texi @@ -22526,6 +22526,234 @@ and Error. @node Telephony Services @subsection Telephony Services +@cindex telephony, services +The @code{(gnu services telephony)} module contains Guix service +definitions for telephony services. Currently it provides the following +services: + +@subsubheading Jami + +@cindex jami, service + +This section describes how to configure a Jami server that can be used +to host video (or audio) conferences, among other uses. The following +example demonstrates how to specify Jami account archives (backups) to +be provisioned automatically: + +@lisp +(service jami-service-type + (jami-configuration + (accounts + (list (jami-account + (archive "/etc/jami/unencrypted-account-1.gz")) + (jami-account + (archive "/etc/jami/unencrypted-account-2.gz")))))) +@end lisp + +When the accounts field is specified, the Jami account files of the +service found under @file{/var/lib/jami} are recreated every time the +service starts. + +Jami accounts and their corresponding backup archives can be generated +using either the @code{jami-qt} or @code{jami-gnome} Jami clients. The +accounts should not be password-protected, but it is wise to ensure +their files are only readable by @samp{root}. + +The next example shows how to declare that only some contacts should be +allowed to communicate with a given account: + +@lisp +(service jami-service-type + (jami-configuration + (accounts + (list (jami-account + (archive "/etc/jami/unencrypted-account-1.gz") + (peer-discovery? #t) + (rendezvous-point? #t) + (allowed-contacts + '("1dbcb0f5f37324228235564b79f2b9737e9a008f" + "2dbcb0f5f37324228235564b79f2b9737e9a008f"))))))) +@end lisp + +In this mode, only the declared @code{allowed-contacts} can initiate +communication with the Jami account. This can be used, for example, +with rendezvous point accounts to create a private video conferencing +space. + +To put the system administrator in full control of the conferences +hosted on their system, the Jami service supports the following actions: + +@example sh +# herd doc jami list-actions jami +(list-accounts + list-account-details + list-banned-contacts + list-contacts + list-moderators + add-moderator + ban-contact + enable-account + disable-account) +@end example + +The above actions aim to provide the most valuable actions for +moderation purposes, not to cover the whole Jami API. Users wanting to +interact with the Jami daemon from Guile may be interested in +experimenting with the @code{(gnu build jami-service)} module, which +powers the above Shepherd actions. + +@c TODO: This should be auto-generated from the doc already defined on +@c the shepherd-actions themselves in (gnu services telephony). +The @code{add-moderator} and @code{ban-contact} actions accept a contact +@emph{fingerprint} (40 characters long hash) as first argument and an +account fingerprint or username as second argument: + +@example sh +# herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f \ + f3345f2775ddfe07a4b0d95daea111d15fbc1199 + +# herd list-moderators jami +Moderators for account f3345f2775ddfe07a4b0d95daea111d15fbc1199: + - 1dbcb0f5f37324228235564b79f2b9737e9a008f + +@end example + +In the case of @code{ban-contact}, the second username argument is +optional; when omitted, the account is banned from all Jami accounts: + +@example sh +# herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f + +# herd list-banned-contacts jami +Banned contacts for account f3345f2775ddfe07a4b0d95daea111d15fbc1199: + - 1dbcb0f5f37324228235564b79f2b9737e9a008f + +@end example + +Banned contacts are also stripped from their moderation privileges. + +The @code{disable-account} action allows to completely disconnect an +account from the network, making it unreachable, while +@code{enable-account} does the inverse. They accept a single account +username or fingerprint as first argument: + +@example sh +# herd disable-account jami f3345f2775ddfe07a4b0d95daea111d15fbc1199 + +# herd list-accounts jami +The following Jami accounts are available: + - f3345f2775ddfe07a4b0d95daea111d15fbc1199 (dummy) [disabled] + +@end example + +The @code{list-account-details} action prints the detailed parameters of +each accounts in the Recutils format, which means the @command{recsel} +command can be used to select accounts of interest (@pxref{Selection +Expressions,,,recutils, GNU recutils manual}). Note that period +characters (@samp{.}) found in the account parameter keys are mapped to +underscores (@samp{_}) in the output, to meet the requirements of the +Recutils format. The following example shows how to print the account +fingerprints for all accounts operating in the rendezvous point mode: + +@example sh +# herd list-account-details jami | \ + recsel -p Account.username -e 'Account.rendezVous ~ "true"' +Account_username: f3345f2775ddfe07a4b0d95daea111d15fbc1199 +@end example + +The remaining actions should be self-explanatory. + +The complete set of available configuration options is detailed below. + +@c TODO: Ideally, the following fragments would be auto-generated at +@c build time, so that they needn't be manually duplicated. +@c Auto-generated via (configuration->documentation 'jami-configuration) +@deftp {Data Type} jami-configuration +Available @code{jami-configuration} fields are: + +@table @asis +@item @code{jamid} (default: @code{libring}) (type: package) +The Jami daemon package to use. + +@item @code{dbus} (default: @code{dbus}) (type: package) +The D-Bus package to use to start the required D-Bus session. + +@item @code{nss-certs} (default: @code{nss-certs}) (type: package) +The nss-certs package to use to provide TLS certificates. + +@item @code{enable-logging?} (default: @code{#t}) (type: boolean) +Whether to enable logging to syslog. + +@item @code{debug?} (default: @code{#f}) (type: boolean) +Whether to enable debug level messages. + +@item @code{auto-answer?} (default: @code{#f}) (type: boolean) +Whether to force automatic answer to incoming calls. + +@item @code{accounts} (default: @code{disabled}) (type: maybe-jami-account-list) +A list of Jami accounts to be (re-)provisioned every time the Jami +daemon service starts. When providing this field, the account +directories under @file{/var/lib/jami/} are recreated every time the +service starts, ensuring a consistent state. + +@end table + +@end deftp + +@c Auto-generated via (configuration->documentation 'jami-account) +@deftp {Data Type} jami-account +Available @code{jami-account} fields are: + +@table @asis +@item @code{archive} (type: string-or-computed-file) +The account archive (backup) file name of the account. This is used to +provision the account when the service starts. The account archive +should @emph{not} be encrypted. It is highly recommended to make it +readable only to the @samp{root} user (i.e., not in the store), to guard +against leaking the secret key material of the Jami account it contains. + +@item @code{allowed-contacts} (default: @code{disabled}) (type: maybe-account-fingerprint-list) +The list of allowed contacts for the account, entered as their 40 +characters long fingerprint. Messages or calls from accounts not in +that list will be rejected. When unspecified, the configuration of the +account archive is used as-is with respect to contacts and public +inbound calls/messaging allowance, which typically defaults to allow any +contact to communicate with the account. + +@item @code{moderators} (default: @code{disabled}) (type: maybe-account-fingerprint-list) +The list of contacts that should have moderation privileges (to ban, +mute, etc. other users) in rendezvous conferences, entered as their 40 +characters long fingerprint. When unspecified, the configuration of the +account archive is used as-is with respect to moderation, which +typically defaults to allow anyone to moderate. + +@item @code{rendezvous-point?} (default: @code{disabled}) (type: maybe-boolean) +Whether the account should operate in the rendezvous mode. In this +mode, all the incoming audio/video calls are mixed into a conference. +When left unspecified, the value from the account archive prevails. + +@item @code{peer-discovery?} (default: @code{disabled}) (type: maybe-boolean) +Whether peer discovery should be enabled. Peer discovery is used to +discover other OpenDHT nodes on the local network, which can be useful +to maintain communication between devices on such network even when the +connection to the the Internet has been lost. When left unspecified, +the value from the account archive prevails. + +@item @code{bootstrap-hostnames} (default: @code{disabled}) (type: maybe-string-list) +A list of hostnames or IPs pointing to OpenDHT nodes, that should be +used to initially join the OpenDHT network. When left unspecified, the +value from the account archive prevails. + +@item @code{name-server-uri} (default: @code{disabled}) (type: maybe-string) +The URI of the name server to use, that can be used to retrieve the +account fingerprint for a registered username. + +@end table + +@end deftp + +@subsubheading Murmur (VoIP server) + @cindex Murmur (VoIP server) @cindex VoIP server This section describes how to set up and run a Murmur server. Murmur is diff --git a/gnu/build/jami-service.scm b/gnu/build/jami-service.scm new file mode 100644 index 0000000000..d44e87387d --- /dev/null +++ b/gnu/build/jami-service.scm @@ -0,0 +1,587 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Maxim Cournoyer +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix 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 General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +;;; Commentary: +;;; +;;; This module contains helpers used as part of the jami-service-type +;;; definition. +;;; +;;; Code: + +(define-module (gnu build jami-service) + #:use-module (ice-9 format) + #:use-module (ice-9 match) + #:use-module (ice-9 peg) + #:use-module (ice-9 rdelim) + #:use-module (ice-9 regex) + #:use-module (rnrs io ports) + #:autoload (shepherd service) (fork+exec-command) + #:use-module (srfi srfi-1) + #:use-module (srfi srfi-26) + #:export (account-fingerprint? + account-details->recutil + get-accounts + get-usernames + set-account-details + add-account + account->username + username->account + username->contacts + enable-account + disable-account + + add-contact + remove-contact + + set-all-moderators + set-moderator + username->all-moderators? + username->moderators + + dbus-available-services + dbus-service-available? + + %send-dbus-binary + %send-dbus-bus + %send-dbus-user + %send-dbus-group + %send-dbus-debug + send-dbus + + with-retries)) + +;;; +;;; Utilities. +;;; + +(define-syntax-rule (with-retries n delay body ...) + "Retry the code in BODY up to N times until it doesn't raise an exception +nor return #f, else raise an error. A delay of DELAY seconds is inserted +before each retry." + (let loop ((attempts 0)) + (catch #t + (lambda () + (let ((result (begin body ...))) + (if (not result) + (error "failed attempt" attempts) + result))) + (lambda args + (if (< attempts n) + (begin + (sleep delay) ;else wait and retry + (loop (+ 1 attempts))) + (error "maximum number of retry attempts reached" + body ... args)))))) + +(define (alist->list alist) + "Flatten ALIST into a list." + (append-map (match-lambda + (() '()) + ((key . value) + (list key value))) + alist)) + +(define account-fingerprint-rx (make-regexp "[0-9A-f]{40}")) + +(define (account-fingerprint? val) + "A Jami account fingerprint is 40 characters long and only contains +hexadecimal characters." + (and (string? val) + (regexp-exec account-fingerprint-rx val))) + + +;;; +;;; D-Bus reply parser. +;;; + +(define (parse-dbus-reply reply) + "Return the parse tree of REPLY, a string returned by the 'dbus-send' +command." + ;; Refer to 'man 1 dbus-send' for the grammar reference. Note that the + ;; format of the replies doesn't match the format of the input, which is the + ;; one documented, but it gives an idea. For an even better reference, see + ;; the `print_iter' procedure of the 'dbus-print-message.c' file from the + ;; 'dbus' package sources. + (define-peg-string-patterns + "contents <- header (item / container (item / container*)?) + item <-- WS type WS value NL + container <- array / dict / variant + array <-- array-start (item / container)* array-end + dict <-- array-start dict-entry* array-end + dict-entry <-- dict-entry-start item item dict-entry-end + variant <-- variant-start item + type <-- 'string' / 'int16' / 'uint16' / 'int32' / 'uint32' / 'int64' / + 'uint64' / 'double' / 'byte' / 'boolean' / 'objpath' + value <-- (!NL .)* NL + header < (!NL .)* NL + variant-start < WS 'variant' + array-start < WS 'array [' NL + array-end < WS ']' NL + dict-entry-start < WS 'dict entry(' NL + dict-entry-end < WS ')' NL + DQ < '\"' + WS < ' '* + NL < '\n'*") + + (peg:tree (match-pattern contents reply))) + +(define (strip-quotes text) + "Strip the leading and trailing double quotes (\") characters from TEXT." + (let* ((text* (if (string-prefix? "\"" text) + (string-drop text 1) + text)) + (text** (if (string-suffix? "\"" text*) + (string-drop-right text* 1) + text*))) + text**)) + +(define (deserialize-item item) + "Return the value described by the ITEM parse tree as a Guile object." + ;; Strings are printed wrapped in double quotes (see the print_iter + ;; procedure in dbus-print-message.c). + (match item + (('item ('type "string") ('value value)) + (strip-quotes value)) + (('item ('type "boolean") ('value value)) + (if (string=? "true" value) + #t + #f)) + (('item _ ('value value)) + value))) + +(define (serialize-boolean bool) + "Return the serialized format expected by dbus-send for BOOL." + (format #f "boolean:~:[false~;true~]" bool)) + +(define (dict->alist dict-parse-tree) + "Translate a dict parse tree to an alist." + (define (tuples->alist tuples) + (map (lambda (x) (apply cons x)) tuples)) + + (match dict-parse-tree + ('dict + '()) + (('dict ('dict-entry keys values) ...) + (let ((keys* (map deserialize-item keys)) + (values* (map deserialize-item values))) + (tuples->alist (zip keys* values*)))))) + +(define (array->list array-parse-tree) + "Translate an array parse tree to a list." + (match array-parse-tree + ('array + '()) + (('array items ...) + (map deserialize-item items)))) + + +;;; +;;; Low-level, D-Bus-related procedures. +;;; + +;;; The following parameters are used in the jami-service-type service +;;; definition to conveniently customize the behavior of the send-dbus helper, +;;; even when called indirectly. +(define %send-dbus-binary (make-parameter "dbus-send")) +(define %send-dbus-bus (make-parameter #f)) +(define %send-dbus-user (make-parameter #f)) +(define %send-dbus-group (make-parameter #f)) +(define %send-dbus-debug (make-parameter #f)) + +(define* (send-dbus #:key service path interface method + bus + dbus-send + user group + timeout + arguments) + "Return the response of DBUS-SEND, else raise an error. Unless explicitly +provided, DBUS-SEND takes the value of the %SEND-DBUS-BINARY parameter. BUS +can be used to specify the bus address, such as 'unix:path=/var/run/jami/bus'. +Alternatively, the %SEND-DBUS-BUS parameter can be used. ARGUMENTS can be +used to pass input values to a D-Bus method call. TIMEOUT is the amount of +time to wait for a reply in milliseconds before giving up with an error. USER +and GROUP allow choosing under which user/group the DBUS-SEND command is +executed. Alternatively, the %SEND-DBUS-USER and %SEND-DBUS-GROUP parameters +can be used instead." + (let* ((command `(,(if dbus-send + dbus-send + (%send-dbus-binary)) + ,@(if (or bus (%send-dbus-bus)) + (list (string-append "--bus=" + (or bus (%send-dbus-bus)))) + '()) + "--print-reply" + ,@(if timeout + (list (format #f "--reply-timeout=~d" timeout)) + '()) + ,(string-append "--dest=" service) ;e.g., cx.ring.Ring + ,path ;e.g., /cx/ring/Ring/ConfigurationManager + ,(string-append interface "." method) + ,@(or arguments '()))) + (temp-port (mkstemp! (string-copy "/tmp/dbus-send-output-XXXXXXX"))) + (temp-file (port-filename temp-port))) + (dynamic-wind + (lambda () + (let* ((uid (or (and=> (or user (%send-dbus-user)) + (compose passwd:uid getpwnam)) -1)) + (gid (or (and=> (or group (%send-dbus-group)) + (compose group:gid getgrnam)) -1))) + (chown temp-port uid gid))) + (lambda () + (let ((pid (fork+exec-command command + #:user (or user (%send-dbus-user)) + #:group (or group (%send-dbus-group)) + #:log-file temp-file))) + (match (waitpid pid) + ((_ . status) + (let ((exit-status (status:exit-val status)) + (output (call-with-port temp-port get-string-all))) + (if (= 0 exit-status) + output + (error "the send-dbus command exited with: " + command exit-status output))))))) + (lambda () + (false-if-exception (delete-file temp-file)))))) + +(define (parse-account-ids reply) + "Return the Jami account IDs from REPLY, which is assumed to be the output +of the Jami D-Bus `getAccountList' method." + (array->list (parse-dbus-reply reply))) + +(define (parse-account-details reply) + "Parse REPLY, which is assumed to be the output of the Jami D-Bus +`getAccountDetails' method, and return its content as an alist." + (dict->alist (parse-dbus-reply reply))) + +(define (parse-contacts reply) + "Parse REPLY, which is assumed to be the output of the Jamid D-Bus +`getContacts' method, and return its content as an alist." + (match (parse-dbus-reply reply) + ('array + '()) + (('array dicts ...) + (map dict->alist dicts)))) + + +;;; +;;; Higher-level, D-Bus-related procedures. +;;; + +(define (validate-fingerprint fingerprint) + "Validate that fingerprint is 40 characters long." + (unless (account-fingerprint? fingerprint) + (error "Account fingerprint is not valid:" fingerprint))) + +(define (dbus-available-services) + "Return the list of available (acquired) D-Bus services." + (let ((reply (parse-dbus-reply + (send-dbus #:service "org.freedesktop.DBus" + #:path "/org/freedesktop/DBus" + #:interface "org.freedesktop.DBus" + #:method "ListNames")))) + ;; Remove entries such as ":1.7". + (remove (cut string-prefix? ":" <>) + (array->list reply)))) + +(define (dbus-service-available? service) + "Predicate to check for the D-Bus SERVICE availability." + (member service (dbus-available-services))) + +(define* (send-dbus/configuration-manager #:key method arguments timeout) + "Query the Jami D-Bus ConfigurationManager service." + (send-dbus #:service "cx.ring.Ring" + #:path "/cx/ring/Ring/ConfigurationManager" + #:interface "cx.ring.Ring.ConfigurationManager" + #:method method + #:arguments arguments + #:timeout timeout)) + +;;; The following methods are for internal use; they make use of the account +;;; ID, an implementation detail of Jami the user should not need to be +;;; concerned with. +(define (get-account-ids) + "Return the available Jami account identifiers (IDs). Account IDs are an +implementation detail used to identify the accounts in Jami." + (parse-account-ids + (send-dbus/configuration-manager #:method "getAccountList"))) + +(define (id->account-details id) + "Retrieve the account data associated with the given account ID." + (parse-account-details + (send-dbus/configuration-manager + #:method "getAccountDetails" + #:arguments (list (string-append "string:" id))))) + +(define (id->volatile-account-details id) + "Retrieve the account data associated with the given account ID." + (parse-account-details + (send-dbus/configuration-manager + #:method "getVolatileAccountDetails" + #:arguments (list (string-append "string:" id))))) + +(define (id->account id) + "Retrieve the complete account data associated with the given account ID." + (append (id->volatile-account-details id) + (id->account-details id))) + +(define %username-to-id-cache #f) + +(define (invalidate-username-to-id-cache!) + (set! %username-to-id-cache #f)) + +(define (username->id username) + "Return the first account ID corresponding to USERNAME." + (unless (assoc-ref %username-to-id-cache username) + (set! %username-to-id-cache + (append-map + (lambda (id) + (let* ((account (id->account id)) + (username (assoc-ref account "Account.username")) + (registered-name (assoc-ref account + "Account.registeredName"))) + `(,@(if username + (list (cons username id)) + '()) + ,@(if registered-name + (list (cons registered-name id)) + '())))) + (get-account-ids)))) + (or (assoc-ref %username-to-id-cache username) + (let ((message (format #f "Could not retrieve a local account ID\ + for ~:[username~;fingerprint~]" (account-fingerprint? username)))) + (error message username)))) + +(define (account->username account) + "Return USERNAME, the registered username associated with ACCOUNT, else its +public key fingerprint." + (or (assoc-ref account "Account.registeredName") + (assoc-ref account "Account.username"))) + +(define (id->username id) + "Return USERNAME, the registered username associated with ID, else its +public key fingerprint, else #f." + (account->username (id->account id))) + +(define (get-accounts) + "Return the list of all accounts, as a list of alists." + (map id->account (get-account-ids))) + +(define (get-usernames) + "Return the list of the usernames associated with the present accounts." + (map account->username (get-accounts))) + +(define (username->account username) + "Return the first account associated with USERNAME, else #f. +USERNAME can be either the account 40 characters public key fingerprint or a +registered username." + (find (lambda (account) + (member username + (list (assoc-ref account "Account.username") + (assoc-ref account "Account.registeredName")))) + (get-accounts))) + +(define (add-account archive) + "Import the Jami account ARCHIVE and return its account ID. The archive +should *not* be encrypted with a password. Return the username associated +with the account." + (invalidate-username-to-id-cache!) + (let ((reply (send-dbus/configuration-manager + #:method "addAccount" + #:arguments (list (string-append + "dict:string:string:Account.archivePath," + archive + ",Account.type,RING"))))) + ;; The account information takes some time to be populated. + (let ((id (deserialize-item (parse-dbus-reply reply)))) + (with-retries 20 1 + (let ((username (id->username id))) + (if (string-null? username) + #f + username)))))) + +(define (remove-account username) + "Delete the Jami account associated with USERNAME, the account 40 characters +fingerprint or a registered username." + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "removeAccount" + #:arguments (list (string-append "string:" id)))) + (invalidate-username-to-id-cache!)) + +(define* (username->contacts username) + "Return the contacts associated with the account of USERNAME as two values; +the first one being the regular contacts and the second one the banned +contacts. USERNAME can be either the account 40 characters public key +fingerprint or a registered username. The contacts returned are represented +using their 40 characters fingerprint." + (let* ((id (username->id username)) + (reply (send-dbus/configuration-manager + #:method "getContacts" + #:arguments (list (string-append "string:" id)))) + (all-contacts (parse-contacts reply)) + (banned? (lambda (contact) + (and=> (assoc-ref contact "banned") + (cut string=? "true" <>)))) + (banned (filter banned? all-contacts)) + (not-banned (filter (negate banned?) all-contacts)) + (fingerprint (cut assoc-ref <> "id"))) + (values (map fingerprint not-banned) + (map fingerprint banned)))) + +(define* (remove-contact contact username #:key ban?) + "Remove CONTACT, the 40 characters public key fingerprint of a contact, from +the account associated with USERNAME (either a fingerprint or a registered +username). When BAN? is true, also mark the contact as banned." + (validate-fingerprint contact) + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "removeContact" + #:arguments (list (string-append "string:" id) + (string-append "string:" contact) + (serialize-boolean ban?))))) + +(define (add-contact contact username) + "Add CONTACT, the 40 characters public key fingerprint of a contact, to the +account of USERNAME (either a fingerprint or a registered username)." + (validate-fingerprint contact) + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "addContact" + #:arguments (list (string-append "string:" id) + (string-append "string:" contact))))) + +(define* (set-account-details details username #:key timeout) + "Set DETAILS, an alist containing the key value pairs to set for the account +of USERNAME, a registered username or account fingerprint. The value of the +parameters not provided are unchanged. TIMEOUT is a value in milliseconds to +pass to the `send-dbus/configuration-manager' procedure." + (let* ((id (username->id username)) + (current-details (id->account-details id)) + (updated-details (map (match-lambda + ((key . value) + (or (and=> (assoc-ref details key) + (cut cons key <>)) + (cons key value)))) + current-details)) + ;; dbus-send does not permit sending null strings (it throws a + ;; "malformed dictionary" error). Luckily they seem to have the + ;; semantic of "default account value" in Jami; so simply drop them. + (updated-details* (remove (match-lambda + ((_ . value) + (string-null? value))) + updated-details))) + (send-dbus/configuration-manager + #:timeout timeout + #:method "setAccountDetails" + #:arguments + (list (string-append "string:" id) + (string-append "dict:string:string:" + (string-join (alist->list updated-details*) + ",")))))) + +(define (set-all-moderators enabled? username) + "Set the 'AllModerators' property to enabled? for the account of USERNAME, a +registered username or account fingerprint." + (let ((id (username->id username))) + (send-dbus/configuration-manager + #:method "setAllModerators" + #:arguments + (list (string-append "string:" id) + (serialize-boolean enabled?))))) + +(define (username->all-moderators? username) + "Return the 'AllModerators' property for the account of USERNAME, a +registered username or account fingerprint." + (let* ((id (username->id username)) + (reply (send-dbus/configuration-manager + #:method "isAllModerators" + #:arguments + (list (string-append "string:" id))))) + (deserialize-item (parse-dbus-reply reply)))) + +(define (username->moderators username) + "Return the moderators for the account of USERNAME, a registered username or +account fingerprint." + (let* ((id (username->id username)) + (reply (send-dbus/configuration-manager + #:method "getDefaultModerators" + #:arguments + (list (string-append "string:" id))))) + (array->list (parse-dbus-reply reply)))) + +(define (set-moderator contact enabled? username) + "Set the moderator flag to ENABLED? for CONTACT, the 40 characters public +key fingerprint of a contact for the account of USERNAME, a registered +username or account fingerprint." + (validate-fingerprint contact) + (let* ((id (username->id username))) + (send-dbus/configuration-manager #:method "setDefaultModerator" + #:arguments + (list (string-append "string:" id) + (string-append "string:" contact) + (serialize-boolean enabled?))))) + +(define (disable-account username) + "Disable the account known by USERNAME, a registered username or account +fingerprint." + (set-account-details '(("Account.enable" . "false")) username + ;; Waiting for the reply on this command takes a very + ;; long time that trips the default D-Bus timeout value + ;; (25 s), for some reason. + #:timeout 60000)) + +(define (enable-account username) + "Enable the account known by USERNAME, a registered username or account +fingerprint." + (set-account-details '(("Account.enable" . "true")) username)) + + +;;; +;;; Presentation procedures. +;;; + +(define (.->_ text) + "Map each period character to underscore characters." + (string-map (match-lambda + (#\. #\_) + (c c)) + text)) + +(define (account-details->recutil account-details) + "Serialize the account-details alist into a recutil string. Period +characters in the keys are normalized to underscore to meet Recutils' format +requirements." + (define (pair->recutil-property pair) + (match pair + ((key . value) + (string-append (.->_ key) ": " value)))) + + (define sorted-account-details + ;; Have the account username, display name and alias appear first, for + ;; convenience. + (let ((first-items '("Account.username" + "Account.displayName" + "Account.alias"))) + (append (map (cut assoc <> account-details) first-items) + (fold alist-delete account-details first-items)))) + + (string-join (map pair->recutil-property sorted-account-details) "\n")) + +;; Local Variables: +;; eval: (put 'with-retries 'scheme-indent-function 2) +;; End: diff --git a/gnu/local.mk b/gnu/local.mk index 9e8f2d702d..e8494806fd 100644 --- a/gnu/local.mk +++ b/gnu/local.mk @@ -685,6 +685,7 @@ GNU_SYSTEM_MODULES = \ %D%/build/chromium-extension.scm \ %D%/build/cross-toolchain.scm \ %D%/build/image.scm \ + %D%/build/jami-service.scm \ %D%/build/file-systems.scm \ %D%/build/hurd-boot.scm \ %D%/build/install.scm \ @@ -722,6 +723,7 @@ GNU_SYSTEM_MODULES = \ %D%/tests/security-token.scm \ %D%/tests/singularity.scm \ %D%/tests/ssh.scm \ + %D%/tests/telephony.scm \ %D%/tests/version-control.scm \ %D%/tests/virtualization.scm \ %D%/tests/web.scm @@ -787,7 +789,8 @@ dist_installer_DATA = \ MODULES_NOT_COMPILED += \ %D%/build/locale.scm \ %D%/build/shepherd.scm \ - %D%/build/svg.scm + %D%/build/svg.scm \ + %D%/tests/data/jami-dummy-account.dat patchdir = $(guilemoduledir)/%D%/packages/patches dist_patch_DATA = \ diff --git a/gnu/services/telephony.scm b/gnu/services/telephony.scm index e1259cc2df..fd90840324 100644 --- a/gnu/services/telephony.scm +++ b/gnu/services/telephony.scm @@ -1,5 +1,6 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2017 nee +;;; Copyright © 2021 Maxim Cournoyer ;;; ;;; This file is part of GNU Guix. ;;; @@ -17,16 +18,45 @@ ;;; along with GNU Guix. If not, see . (define-module (gnu services telephony) - #:use-module (gnu services) + #:use-module ((gnu build jami-service) #:select (account-fingerprint?)) + #:use-module ((gnu services) #:hide (delete)) + #:use-module (gnu services configuration) #:use-module (gnu services shepherd) #:use-module (gnu system shadow) #:use-module (gnu packages admin) + #:use-module (gnu packages certs) + #:use-module (gnu packages glib) + #:use-module (gnu packages jami) #:use-module (gnu packages telephony) #:use-module (guix records) + #:use-module (guix modules) + #:use-module (guix packages) #:use-module (guix gexp) #:use-module (srfi srfi-1) + #:use-module (srfi srfi-2) + #:use-module (srfi srfi-26) + #:use-module (ice-9 format) #:use-module (ice-9 match) - #:export (murmur-configuration + #:export (jami-account + jami-account-archive + jami-account-allowed-contacts + jami-account-moderators + jami-account-rendezvous-point? + jami-account-discovery? + jami-account-bootstrap-uri + jami-account-name-server-uri + + jami-configuration + jami-configuration-jamid + jami-configuration-dbus + jami-configuration-enable-logging? + jami-configuration-debug? + jami-configuration-auto-answer? + jami-configuration-accounts + + jami-service-type + + murmur-configuration make-murmur-configuration murmur-configuration? murmur-configuration-package @@ -74,6 +104,652 @@ (define-module (gnu services telephony) murmur-service-type)) + +;;; +;;; Jami daemon. +;;; + +;;; XXX: Passing a computed-file object as the account is used for tests. +(define (string-or-computed-file? val) + (or (string? val) + (computed-file? val))) + +(define (string-list? val) + (and (list? val) + (and-map string? val))) + +(define (account-fingerprint-list? val) + (and (list? val) + (and-map account-fingerprint? val))) + +(define-maybe string-list) + +(define-maybe/no-serialization account-fingerprint-list) + +(define-maybe boolean) + +(define-maybe string) + +;;; The following serializers are used to derive an account details alist from +;;; a record. +(define (serialize-string-list _ val) + (string-join val ";")) + +(define (serialize-boolean _ val) + (format #f "~:[false~;true~]" val)) + +(define (serialize-string _ val) + val) + +;;; Note: Serialization is used to produce an account details alist that can +;;; be passed to the SET-ACCOUNT-DETAILS procedure. Fields that do not map to +;;; a Jami account 'detail' should have their serialization disabled via the +;;; 'empty-serializer' procedure. +(define-configuration jami-account + (archive + (string-or-computed-file) + "The account archive (backup) file name of the account. This is used to +provision the account when the service starts. The account archive should +@emph{not} be encrypted. It is highly recommended to make it readable only to +the @samp{root} user (i.e., not in the store), to guard against leaking the +secret key material of the Jami account it contains." + empty-serializer) + (allowed-contacts + (maybe-account-fingerprint-list 'disabled) + "The list of allowed contacts for the account, entered as their 40 +characters long fingerprint. Messages or calls from accounts not in that list +will be rejected. When unspecified, the configuration of the account archive +is used as-is with respect to contacts and public inbound calls/messaging +allowance, which typically defaults to allow any contact to communicate with +the account." + empty-serializer) + (moderators + (maybe-account-fingerprint-list 'disabled) + "The list of contacts that should have moderation privileges (to ban, mute, +etc. other users) in rendezvous conferences, entered as their 40 characters +long fingerprint. When unspecified, the configuration of the account archive +is used as-is with respect to moderation, which typically defaults to allow +anyone to moderate." + empty-serializer) + ;; The serializable fields below are to be set with set-account-details. + (rendezvous-point? + (maybe-boolean 'disabled) + "Whether the account should operate in the rendezvous mode. In this mode, +all the incoming audio/video calls are mixed into a conference. When left +unspecified, the value from the account archive prevails.") + (peer-discovery? + (maybe-boolean 'disabled) + "Whether peer discovery should be enabled. Peer discovery is used to +discover other OpenDHT nodes on the local network, which can be useful to +maintain communication between devices on such network even when the +connection to the the Internet has been lost. When left unspecified, the +value from the account archive prevails.") + (bootstrap-hostnames + (maybe-string-list 'disabled) + "A list of hostnames or IPs pointing to OpenDHT nodes, that should be used +to initially join the OpenDHT network. When left unspecified, the value from +the account archive prevails.") + (name-server-uri + (maybe-string 'disabled) + "The URI of the name server to use, that can be used to retrieve the +account fingerprint for a registered username.")) + +(define (jami-account->alist jami-account-object) + "Serialize the JAMI-ACCOUNT object as an alist suitable to be passed to +SET-ACCOUNT-DETAILS." + (define (field-name->account-detail name) + (match name + ('rendezvous-point? "Account.rendezVous") + ('peer-discovery? "Account.peerDiscovery") + ('bootstrap-hostnames "Account.hostname") + ('name-server-uri "RingNS.uri") + (_ #f))) + + (filter-map (lambda (field) + (and-let* ((name (field-name->account-detail + (configuration-field-name field))) + (value ((configuration-field-serializer field) + name ((configuration-field-getter field) + jami-account-object))) + ;; The define-maybe default serializer produces an + ;; empty string for the 'disabled value. + (value* (if (string-null? value) + #f + value))) + (cons name value*))) + jami-account-fields)) + +(define (jami-account-list? val) + (and (list? val) + (and-map jami-account? val))) + +(define-maybe/no-serialization jami-account-list) + +(define-configuration/no-serialization jami-configuration + (jamid + (package libring) + "The Jami daemon package to use.") + (dbus + (package dbus) + "The D-Bus package to use to start the required D-Bus session.") + (nss-certs + (package nss-certs) + "The nss-certs package to use to provide TLS certificates.") + (enable-logging? + (boolean #t) + "Whether to enable logging to syslog.") + (debug? + (boolean #f) + "Whether to enable debug level messages.") + (auto-answer? + (boolean #f) + "Whether to force automatic answer to incoming calls.") + (accounts + (maybe-jami-account-list 'disabled) + "A list of Jami accounts to be (re-)provisioned every time the Jami daemon +service starts. When providing this field, the account directories under +@file{/var/lib/jami/} are recreated every time the service starts, ensuring a +consistent state.")) + +(define %jami-accounts + (list (user-group (name "jami") (system? #t)) + (user-account + (name "jami") + (group "jami") + (system? #t) + (comment "Jami daemon user") + (home-directory "/var/lib/jami")))) + +(define (jami-configuration->command-line-arguments config) + "Derive the command line arguments to used to launch the Jami daemon from +CONFIG, a object." + (match-record config + (jamid dbus enable-logging? debug? auto-answer?) + `(,(file-append jamid "/lib/ring/dring") + "--persistent" ;stay alive after client quits + ,@(if enable-logging? + '() ;logs go to syslog by default + (list "--console")) ;else stdout/stderr + ,@(if debug? + (list "--debug") + '()) + ,@(if auto-answer? + (list "--auto-answer") + '())))) + +(define (jami-dbus-session-activation config) + "Create a directory to hold the Jami D-Bus session socket." + (with-imported-modules (source-module-closure '((gnu build activation))) + #~(begin + (use-modules (gnu build activation)) + (let ((user (getpwnam "jami"))) + (mkdir-p/perms "/var/run/jami" user #o700))))) + +(define (jami-shepherd-services config) + "Return a running the Jami daemon." + (let* ((jamid (jami-configuration-jamid config)) + (nss-certs (jami-configuration-nss-certs config)) + (dbus (jami-configuration-dbus config)) + (dbus-daemon (file-append dbus "/bin/dbus-daemon")) + (dbus-send (file-append dbus "/bin/dbus-send")) + (accounts (jami-configuration-accounts config)) + (declarative-mode? (not (eq? 'disabled accounts)))) + + (with-imported-modules (source-module-closure + '((gnu build jami-service) + (gnu build shepherd) + (gnu system file-systems))) + + (define list-accounts-action + (shepherd-action + (name 'list-accounts) + (documentation "List the available Jami accounts. Return the account +details alists keyed by their account username.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + ;; Print the accounts summary or long listing, according to + ;; user-provided option. + (let* ((usernames (get-usernames)) + (accounts (map-in-order username->account usernames))) + (match accounts + (() ;empty list + (format #t "There is no Jami account available.~%")) + ((one two ...) + (format #t "The following Jami accounts are available:~%") + (for-each + (lambda (account) + (define fingerprint (assoc-ref account + "Account.username")) + (define human-friendly-name + (or (assoc-ref account + "Account.registeredName") + (assoc-ref account + "Account.displayName") + (assoc-ref account + "Account.alias"))) + (define disabled? + (and=> (assoc-ref account "Account.enable") + (cut string=? "false" <>))) + + (format #t " - ~a~@[ (~a)~] ~:[~;[disabled]~]~%" + fingerprint human-friendly-name disabled?)) + accounts) + (display "\n"))) + ;; Return the account-details-list alist. + (map cons usernames accounts))))))) + + (define list-account-details-action + (shepherd-action + (name 'list-account-details) + (documentation "Display the account details of the available Jami +accounts in the @code{recutils} format. Return the account details alists +keyed by their account username.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let* ((usernames (get-usernames)) + (accounts (map-in-order username->account usernames))) + (for-each (lambda (account) + (display (account-details->recutil account)) + (display "\n\n")) + accounts) + (map cons usernames accounts))))))) + + (define list-contacts-action + (shepherd-action + (name 'list-contacts) + (documentation "Display the contacts for each Jami account. Return +an alist containing the contacts keyed by the account usernames.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let* ((usernames (get-usernames)) + (contacts (map-in-order username->contacts usernames))) + (for-each (lambda (username contacts) + (format #t "Contacts for account ~a:~%" + username) + (format #t "~{ - ~a~%~}~%" contacts)) + usernames contacts) + (map cons usernames contacts))))))) + + (define list-moderators-action + (shepherd-action + (name 'list-moderators) + (documentation "Display the moderators for each Jami account. Return +an alist containing the moderators keyed by the account usernames.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let* ((usernames (get-usernames)) + (moderators (map-in-order username->moderators + usernames))) + (for-each + (lambda (username moderators) + (if (username->all-moderators? username) + (format #t "Anyone can moderate for account ~a~%" + username) + (begin + (format #t "Moderators for account ~a:~%" username) + (format #t "~{ - ~a~%~}~%" moderators)))) + usernames moderators) + (map cons usernames moderators))))))) + + (define add-moderator-action + (shepherd-action + (name 'add-moderator) + (documentation "Add a moderator for a given Jami account. The +MODERATOR contact must be given as its 40 characters fingerprint, while the +Jami account can be provided as its registered USERNAME or fingerprint. + +@example +herd add-moderator jami 1dbcb0f5f37324228235564b79f2b9737e9a008f username +@end example + +Return the moderators for the account known by USERNAME.") + (procedure + #~(lambda (_ moderator username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (set-all-moderators #f username) + (add-contact moderator username) + (set-moderator moderator #t username) + (username->moderators username)))))) + + (define ban-contact-action + (shepherd-action + (name 'ban-contact) + (documentation "Ban a contact for a given or all Jami accounts, and +clear their moderator flag. The CONTACT must be given as its 40 characters +fingerprint, while the Jami account can be provided as its registered USERNAME +or fingerprint, or omitted. When the account is omitted, CONTACT is banned +from all accounts. + +@example +herd ban-contact jami 1dbcb0f5f37324228235564b79f2b9737e9a008f [username] +@end example") + (procedure + #~(lambda* (_ contact #:optional username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (let ((usernames (or (and=> username list) + (get-usernames)))) + (for-each (lambda (username) + (set-moderator contact #f username) + (remove-contact contact username #:ban? #t)) + usernames))))))) + + (define list-banned-contacts-action + (shepherd-action + (name 'list-banned-contacts) + (documentation "List the banned contacts for each accounts. Return +an alist of the banned contacts, keyed by the account usernames.") + (procedure + #~(lambda _ + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + + (define banned-contacts + (let ((usernames (get-usernames))) + (map cons usernames + (map-in-order (lambda (x) + (receive (_ banned) + (username->contacts x) + banned)) + usernames)))) + + (for-each (match-lambda + ((username . banned) + (unless (null? banned) + (format #t "Banned contacts for account ~a:~%" + username) + (format #t "~{ - ~a~%~}~%" banned)))) + banned-contacts) + banned-contacts))))) + + (define enable-account-action + (shepherd-action + (name 'enable-account) + (documentation "Enable an account. It takes USERNAME as an argument, +either a registered username or the fingerprint of the account.") + (procedure + #~(lambda (_ username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (enable-account username)))))) + + (define disable-account-action + (shepherd-action + (name 'disable-account) + (documentation "Disable an account. It takes USERNAME as an +argument, either a registered username or the fingerprint of the account.") + (procedure + #~(lambda (_ username) + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + (disable-account username)))))) + + (list (shepherd-service + (documentation "Run a D-Bus session for the Jami daemon.") + (provision '(jami-dbus-session)) + (modules `((gnu build shepherd) + (gnu build jami-service) + (gnu system file-systems) + ,@%default-modules)) + ;; The requirement on dbus-system is to ensure other required + ;; activation for D-Bus, such as a /etc/machine-id file. + (requirement '(dbus-system syslogd)) + (start + #~(lambda args + (define pid + ((make-forkexec-constructor/container + (list #$dbus-daemon "--session" + "--address=unix:path=/var/run/jami/bus" + "--nofork" "--syslog-only" "--nopidfile") + #:mappings (list (file-system-mapping + (source "/dev/log") ;for syslog + (target source)) + (file-system-mapping + (source "/var/run/jami") + (target source) + (writable? #t))) + #:user "jami" + #:group "jami" + #:environment-variables + ;; This is so that the cx.ring.Ring service D-Bus + ;; definition is found by dbus-send. + (list (string-append "XDG_DATA_DIRS=" + #$jamid "/share"))))) + + ;; XXX: This manual synchronization probably wouldn't be + ;; needed if we were using a PID file, but providing it via a + ;; customized config file with would not override + ;; the one inherited from the base config of D-Bus. + (let ((sock (socket PF_UNIX SOCK_STREAM 0))) + (with-retries 20 1 (catch 'system-error + (lambda () + (connect sock AF_UNIX + "/var/run/jami/bus") + (close-port sock) + #t) + (lambda args + #f)))) + + pid)) + (stop #~(make-kill-destructor))) + + (shepherd-service + (documentation "Run the Jami daemon.") + (provision '(jami)) + (actions (list list-accounts-action + list-account-details-action + list-contacts-action + list-moderators-action + add-moderator-action + ban-contact-action + list-banned-contacts-action + enable-account-action + disable-account-action)) + (requirement '(jami-dbus-session)) + (modules `((ice-9 format) + (ice-9 ftw) + (ice-9 match) + (ice-9 receive) + (srfi srfi-1) + (srfi srfi-26) + (gnu build jami-service) + (gnu build shepherd) + (gnu system file-systems) + ,@%default-modules)) + (start + #~(lambda args + (define (delete-file-recursively/safe file) + ;; Ensure we're not deleting things outside of + ;; /var/lib/jami. This prevents a possible attack in case + ;; the daemon is compromised and an attacker gains write + ;; access to /var/lib/jami. + (let ((parent-directory (dirname file))) + (if (eq? 'symlink (stat:type (stat parent-directory))) + (error "abnormality detected; unexpected symlink found at" + parent-directory) + (delete-file-recursively file)))) + + (when #$declarative-mode? + ;; Clear the Jami configuration and accounts, to enforce the + ;; declared state. + (catch #t + (lambda () + (for-each (cut delete-file-recursively/safe <>) + '("/var/lib/jami/.cache/jami" + "/var/lib/jami/.config/jami" + "/var/lib/jami/.local/share/jami" + "/var/lib/jami/accounts"))) + (lambda args + #t)) + ;; Copy the Jami account archives from somewhere readable + ;; by root to a place only the jami user can read. + (let* ((accounts-dir "/var/lib/jami/accounts/") + (pwd (getpwnam "jami")) + (user (passwd:uid pwd)) + (group (passwd:gid pwd))) + (mkdir-p accounts-dir) + (chown accounts-dir user group) + (for-each (lambda (f) + (let ((dest (string-append accounts-dir + (basename f)))) + (copy-file f dest) + (chown dest user group))) + '#$(and declarative-mode? + (map jami-account-archive accounts))))) + + ;; Start the daemon. + (define daemon-pid + ((make-forkexec-constructor/container + '#$(jami-configuration->command-line-arguments config) + #:mappings + (list (file-system-mapping + (source "/dev/log") ;for syslog + (target source)) + (file-system-mapping + (source "/var/lib/jami") + (target source) + (writable? #t)) + (file-system-mapping + (source "/var/run/jami") + (target source) + (writable? #t)) + ;; Expose TLS certificates for GnuTLS. + (file-system-mapping + (source #$(file-append nss-certs "/etc/ssl/certs")) + (target "/etc/ssl/certs"))) + #:user "jami" + #:group "jami" + #:environment-variables + (list (string-append "DBUS_SESSION_BUS_ADDRESS=" + "unix:path=/var/run/jami/bus") + ;; Expose TLS certificates for OpenSSL. + "SSL_CERT_DIR=/etc/ssl/certs")))) + + (parameterize ((%send-dbus-binary #$dbus-send) + (%send-dbus-bus "unix:path=/var/run/jami/bus") + (%send-dbus-user "jami") + (%send-dbus-group "jami")) + + ;; Wait until the service name has been acquired by D-Bus. + (with-retries 20 1 + (dbus-service-available? "cx.ring.Ring")) + + (when #$declarative-mode? + ;; Provision the accounts via the D-Bus API of the daemon. + (let* ((jami-account-archives + (map (cut string-append + "/var/lib/jami/accounts/" <>) + (scandir "/var/lib/jami/accounts/" + (lambda (f) + (not (member f '("." ".."))))))) + (usernames (map-in-order (cut add-account <>) + jami-account-archives))) + + (define (archive-name->username archive) + (list-ref + usernames + (list-index (lambda (f) + (string-suffix? (basename archive) f)) + jami-account-archives))) + + (for-each + (lambda (archive allowed-contacts moderators + account-details) + (let ((username (archive-name->username + archive))) + (when (not (eq? 'disabled allowed-contacts)) + ;; Reject calls from unknown contacts. + (set-account-details + '(("DHT.PublicInCalls" . "false")) username) + ;; Remove all contacts. + (for-each (cut remove-contact <> username) + (username->contacts username)) + ;; Add allowed ones. + (for-each (cut add-contact <> username) + allowed-contacts)) + (when (not (eq? 'disabled moderators)) + ;; Disable the 'AllModerators' property. + (set-all-moderators #f username) + ;; Remove all moderators. + (for-each (cut set-moderator <> #f username) + (username->moderators username)) + ;; Add declared moderators. + (for-each (cut set-moderator <> #t username) + moderators)) + ;; Set the various account parameters. + (set-account-details account-details username))) + '#$(and declarative-mode? + (map-in-order (cut jami-account-archive <>) + accounts)) + '#$(and declarative-mode? + (map-in-order + (cut jami-account-allowed-contacts <>) + accounts)) + '#$(and declarative-mode? + (map-in-order (cut jami-account-moderators <>) + accounts)) + '#$(and declarative-mode? + (map-in-order jami-account->alist accounts)))))) + + ;; Finally, return the PID of the daemon process. + daemon-pid)) + (stop + #~(lambda (pid . args) + (kill pid SIGKILL) + ;; Wait for the process to exit; this prevents overlapping + ;; processes when issuing 'herd restart'. + (waitpid pid) + #f))))))) + +(define jami-service-type + (service-type + (name 'jami) + (default-value (jami-configuration)) + (extensions + (list (service-extension shepherd-root-service-type + jami-shepherd-services) + (service-extension account-service-type + (const %jami-accounts)) + (service-extension activation-service-type + jami-dbus-session-activation))) + (description "Run the Jami daemon (@command{dring}). This service is +geared toward the use case of hosting Jami rendezvous points over a headless +server. If you use Jami on your local machine, you may prefer to setup a user +Shepherd service for it instead; this way, the daemon will be shared via your +normal user D-Bus session bus."))) + + +;;; +;;; Murmur. +;;; + ;; https://github.com/mumble-voip/mumble/blob/master/scripts/murmur.ini (define-record-type* murmur-configuration @@ -305,3 +981,7 @@ (define murmur-service-type (service-extension account-service-type murmur-accounts))) (default-value (murmur-configuration)))) + +;; Local Variables: +;; eval: (put 'with-retries 'scheme-indent-function 2) +;; End: diff --git a/gnu/tests/data/jami-dummy-account.dat b/gnu/tests/data/jami-dummy-account.dat new file mode 100644 index 0000000000..0e908396ca --- /dev/null +++ b/gnu/tests/data/jami-dummy-account.dat @@ -0,0 +1,392 @@ +;;; -*- mode: scheme; -*- +;;; JSON extracted from an actual Jami account and processed with +;;; Emacs/guile-json. +(define %jami-account-content-sexp + '(("RINGCAKEY" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQURBTkJna3F\ +oa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzBxWUozSkYvTzhQRGEKRnUwRnpRcHBCaD\ +gybGJMdURrNTlVU0I0MUJSaS9kdDZGV1BRN29YOVpsY25vNGZzM2dmUHQ0dU1hRVBkVFBGKwowbGN2Q\ +jc2cytQTEFlcjlOZGpVQzQ2ZXp0UnNiNE9aQXc4ZUk1M3EwSU04QWJFd0o0ZjllLzBmQUFueHgrK3Qw\ +CmZDeGV1YTBUaVBqVHBpZVJMNmpwQkd5UGI2Qk5pU2ViTkZCNzJOMTBnbzI4REVQYlhkNE9CNkN1blZ\ +5RGZKSU0KOC9PRy8rMndNamI4WkRwT3JrYy93U2ZHbnQyZXA3U0xKSkgwOFIzV1FKWklsSndrcGdLTH\ +FRakVwWFNpclN4dAozSkdtdXdBdE9LaXFFTXh1R043elV3ZlNINEtHdkRaUFNkZklZVXJ3eEp4aDJZZ\ +3lobG5RRC9SSnhRN3d4YlJBCjFhMUZVV0FzbDhLODk5cEtESk1GL09VOWZMRUx6QlViblpaRDRmSlg1\ +NmcyTlluUnJobS94NG9FbFk0MFNYMUUKcHYzb01hNnZrVGN5RjJnUFhKL2FkUVJoS0dFaGRjaHBpeDl\ +5UVphaDFCUFBGYW5jNzBMcjhOaDZJeHFNQ1hiMQozMG9vWHpWZmZNMVFOd28rL3hzRnBlRkRqUTAxQ0\ +9pdWZocitKREcyc0txb0o0V0JwYVhubWI1YXVrVWUvV1RKCjAxVmRyaEkvSVExd3V4QzNMMnpac3dVU\ +1NTaDk0aXg1M0hpU3pWbkI5UkxmaVhZUUVCcFEyNHVoRTdiYlo0bm0KZTczZC9zenpPTXMzYUt3OWtW\ +a2VLMTVtYWhSVWZjdEdhSVQxTkhGWUNYYXByaWExakdNdVpmSk1pSUtZUzNidQpMbUhZckF6dEptNDZ\ +0aHpjdnN3NHlhMnFoa2xUUlFJREFRQUJBb0lDQVFDaHZaUm85KzZ5aFhFTHZ6U3FXZHcxCkZGOERibG\ +hIMmhVWkNuV0kxMDM5SmdyRkxMczFSU1krSzg1aFZYMk9hV1VTNk44TmNCYzUyL1hrdFltS09HUFQKM\ +WZqMnE2M3pPcDNSSFdGNWVPMXhNeExRN3JZSDhqMGZZTFFTUytKemdwb3ZRVnJLSXkrb21JSSt3aUN6\ +R1laRApGQUM0ODJzL0J5MHdtRjVjdC9JTEdIeVY3ZXNVUlo1Vi9iL0ltQzUwQ1lDUWpQR2xBb3JkeUx\ +1MHp2NjZZUXc2CkQycTg0VHAyVUg3SExEVmhFNytUbDg4Q04xWll0VGtpSkthbkNpMFVmbStPKzJFM0\ +5HM01hajk1aDl2NktqYkoKUlkxeTNDRTVmQmkyUFNLbVVzRjN3SzdhbzJDRks5MTgybmlxL2FaNm5WO\ +Xc3NmVrRjhEOWUvS1pqUE5ZT0xkaApFczBSL2laV3RpbUx2RHdXQWNWNFNnSFFjNXJvNU9yOEFUS1ZK\ +VmlzZGFuWXkvdUhmVXZWN3U5cDVLK2E4SHU2CllabW13ZTh4bnF5M3V2M0VabE9LY20zTnZvWllVMnJ\ +HUUFQQW1sWWQ4WlRsZGxPa1JCSGxxYzllMmJuSnNTQW8KNUhhS0N3aDJsWmZpalVGNXFrMXNQcm1kN3\ +BlMld2VVV3QmVuSjJnS1ZoTE5VVGtHWmtTWGhzNlV3WWRRMjVtRQppQzl6WjhXNkQ2OXBvb0lsTTVXT\ +01ySEs0Rng1ck9vT05kUHQ4NEk1bTI3cnpnbXM4QnJXVUlGLytZZjJ0bGdmClBIR0V4c3ZCK3JRQk52\ +WHU3dXoxcVdFTlJTL2YwR2E3dVF4ZW5sZ2dubHc5M1pNOW1GWXpXb1RpdWFmdnphTnAKWEsrTEVrV2F\ +RYUs1Q0VaNEhmUlhBUUtDQVFFQTUyK240OUxQODlyQkR2bFdsTkxNanJqTDdSb0xyQ3FVZGpQWQpyT1\ +hZS3ZkTkxyS2NTc1hNdkY4YW5HQng1UG5oVDZGY05ic2dzQ3BUUXowMThZYmcrbUUxQno5ZTdFNTJGZ\ +i9NCk9BbWZqSllXUnZueUtiNnB3SGlvOHlXUXlVVk1zZU1CcmpvWk1kNkpPZEZ0K2JITHBWOS9iSkdR\ +a3NTRE04WTkKbWxGQUlUL0gyNTh1K1ZKTWsrT0prU28zZmJQSk5Ja1Q2WVBKVmNaSnZTRGI5QU83WDF\ +lendCOXVRL0FEblZ6YwpSQkJOUVZaTStZS2ZNWFJBdmFuWnlmWFFwaUxCQW8rRVRPSHJCR1dDRUhtSF\ +RCaEZIMTkyamtxNlcrTStvS0R1Cm1xMitMc2hZWTVFc2NpL2hPOVZjK2FCM0hhaGliME03M092MHFNc\ +WZoTncyU1BncHdRS0NBUUVBeDlaR1gxQnQKL3MwdGtNcGV1QWhlWjFqTklnZmFEY0RRTWlmU0k4QjRx\ +WUhiL1hOREQ5NjFQME9zMDdCN2wzNE1iZ3U2QlNwcwpXdSt0Y1hjSFlqQVJUc0Qzd0pSaVRIb0RSQzR\ +YYkxEa2pHRUVCVzRKbFVqZTA1QWZrU0QrdkZSMkJwZStxQlBLCm5yb3Mwd1BWL3RXa3MzY2VFOUlBTV\ +pWWDhQeFA3RVNXbitVZDJEWkZhcVFLb0JybHZXRXhxelpYUEJSVjhoS3QKcFBqWnFkZXFQLzhUZTBtS\ +zh5MEVreXVXOWhFdGZ2Sm5HWXhMNStrSG9xd2hQVk1tODZ5YlZNVHRQYWJTdCtPUwp0WHhJTE9RMWRN\ +QkFabzRxSnNkUUZJcTJnSHA5WFYwa2ZNUms1ajdJT1Q4c2Z6TlpKVkRNK1k5VHVlSGJXSnduCnZsWld\ +VZ2NVZTlBaWhRS0NBUUVBbFJaK2h1ckUvNGdLR2dWUld5bTRrTEJHM2dTTFJHdGhuQXVtSnlzaFoveE\ +wKZ2l1Wk55bll5L2hRQWpDMjdoUnlxb04rRFRid3hjdGVPOUJ3c1poNzBZOVJROHYwOERGVExMVE43O\ +E56UG5OcApBbXY5TGhzZTYxaFBMZU1qTkNVcVZPV3hyWFRMeWk1YkpCM2Z4SnhlWGJmNU5BMUpudUpz\ +eXF1SC82TWJ0cytKCmhkY3p3WFRjMCtBZVBKOS9nOENQZXdKYkMzRFVBQ2R1VlNHWHo4ZWZxcm1xbDd\ +jbnB5ZzBpK2pJRkNpVU8rVEcKVFcxeDg3KzUvUFF2MGtSQ0Z1UUloZ2ZCNkcwWW9vcHBrUWRZdXhKZl\ +pPaHdUUldpbTVMMlF5K294WWZySGVQOQozSlltbGFCMmJiN3kxL1FoQjcvek9VMk1nTEtYdHl4Z09ve\ +EpoQlFwZ1FLQ0FRQkIwSUE4dy9CMkNuMEhRcDhQClhUSTZOelRZRUYzd1NhQkg1SFdBOE5MTWdNaERJ\ +TUxsWnlPcVFrK1pLSGFMM2llWjFxTGRNS3VmQjNESC9idWcKeXRQb2JBVXNsN0lJSGVjVmZWaVpvMml\ +pRXhHUCtEMlB2UUFtRFVGWU90V3FrT2FPSlV2VmJ5ODhOM1NyeW9lZgo5aHpZUGxMWmxFQWNGR055S3\ +FibjJXOENHaU5LSWhXYW1Zd21UclY3T1pkeUcrTi9GZk40Vms1NkZyc1pCTDQ5CmRYU2xGZ045TTBaZ\ +WNleTEvZEpPRE9lSHNuME5VK0gvNFZEUk1hR1NmelpwSkxJOXE4T2FiSWpVM0ttb24wQTcKdzFWeWNU\ +L1FwYlBxRUFVckt5dytvMzV3MlAyaUZ1czZiMlBvUUxFTGFTRVl6K3R6UEw5UTM1ejNRdGdMQytuagp\ +IUmxCQW9JQkFES0Q4NGhrYkphczlIQ00zanNNSU9kb213ZEMvcktxVUxKNHFWZU5QR0xDY2VpNEdocn\ +dlQnNICnNoN0hibFlZSDN2U2Y3RW1iZEVCb2xlWVJsaUx5ZzJDQU1ZOGpNK0lpUEwydk1yM2Y3SzZPT\ +mU0UkVPUFJSZkcKWlJDcTh4a0ZPQlg1SUJUbkVCV3QzdVdyb1NGY2x4RTdUa2I4VkUyVzExTG9ZNlkw\ +TUNPaHdxN21xYXRUVnNrawpTRDNySmkrTFR6a2Y4OEx1bjZZNjdiaFNOTWpKZkFaUXNQc0FTRkJBUTJ\ +rQnE5alRLZGVuaU4yYTJIbm0xNCtrCnJDeU9ZVE14Q2hQbWNpS25pVy9MWnFUL0U1dlNRUGdBVzc0dT\ +VLazJoSjRBajNjRW9NVEwxSytZbStWYWh2U0cKTi8xOFdYQ1JRQkg1d0p2eXJYczBtT29GQlRnTWg4d\ +z0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=") + ("ringAccountKey" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTk\ +Jna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRRDNCdDRnOUVUdk9EVnYKM3hWV0ZlS\ +1Nnbk5uVEF3S2dYa3IrQ1FhdU8vTGZWM01RenNSNHliL3hoaWhhb1Z2c2FtZ3ZRU1haL0M1R3I5QQpE\ +YlAxbHNHclRCK1pXMC9uMXVEb3hmVWdZRWY3SGtVanJtdVFjUGlFWGlUVkNiY002U0NzdVNrMnRxdE1\ +BNjBNClRacVo2LzAvbkEvblR5TnFNaUJNSmFRN0NUb2xOTTh2Z2tvd0tBRW14OGpJUG9YZEttMnd6MD\ +Z6SnhwU1d4WjUKc0FBTkdSSHU0b2ZXNWJiMERvamtnRzFBYUJ6Nm9uSmdKK1JFSWV1UkpNQWFHYmtzW\ +TQ4Z1Z1b21BVTU0UFNvUApFb3psVGdHd1k0cnhJTGE0V1Y0RVZMVExrR240ZTYrRUYrSjIvRURBUFdN\ +bXREdFpUeURCc2t5bStLaDRUT2xPCjdnN3JlUUhLdDd1R21YU0RnbzVKZ2hOOHNVRFUxL1Z2YmFFcUZ\ +tTDJrZWNGOVlVZmNsUWRGY1ZncmIrMkh5R3AKRVc0b3RkZjlYYzhOMWxrbGk1dFBqRGZuQ1U3OHB6QT\ +BxQmV1SWhZTnF1VjhGSm1NemhXeVFDbE1MUEFYbXFVdwpWYlY0MWduM0NkektuUlVhZXFONXlzOG5KQ\ +mRJNDBleWlYRUlvU0VKcFpyT1Z5ck1icnNCQzltaVFpZUFhSnlBClFvcjFaRGlwMkpZNUFza2phUGQ0\ +NGk0MGNkeFpob1RhNnpzako5UjZScllFYjhWTmZQemFLaElwSXJSd2NpbCsKWjFrbUUwSE1kY21ITXR\ +hbWZhK0l6WUR4dDV6OGVOZys1RGpVVG1MRkdyQUVWRW5hNDcxdllxYnk5UCs1T1cvNQpSdkxtUER2Tj\ +dlUURpTlZENlBYRFk5OTY4bTZaaHdJREFRQUJBb0lDQURjOEkrTCtlNE41OEFqcHV0MmEyeVNqCllxY\ +VFUSWowMW1GTWhOWXMwQUdTTUswQncyMkdleXZwNFl3R1EzdnNIOSsvSkEydXdoYkJzazNpUW9FQmlx\ +Q0EKenZmOWdPcDRFNlk0elV6RitwSmQvRnUwSG4wWHBab0Rhdnp2eFN4djNFeUN3b0puYWZuL1FHeGw\ +xZEhoQUtsKwpmZGZjekRCc3NPZ1Y2cGtBd1MyY2wwOHFOT2g3cVhaQWFkYk1sQ1lWM0owU1hhaVZiNz\ +lHZXNvTzNwUVBMUUZiClNjQjFjT2sxYnNxWkpOU244d0xmMis5QVBEdzMwWEtNNHg5eTdRTE42Q3oxQ\ +WpvcFJLQ0NIS3R1SEc4UmVETTIKcnRTbjJmTnltQ0VqeDZGVTB6MHFldDV3Y01UbU5weEZuYXdEMU5s\ +dFpnZXBsSllwTjVKZXNEUmo2cFlnWXBPKwo5UDU2cEdtZVNTVzVxcHRoWFFLQmFsdy8wWXp2YUlYdHp\ +hTnZyNUJzRFNrWkU3cldzODIvQmFzUE1RckFmOGpLClZFMU9pSzcrVllUVTRFVWVoZ0FZOXdzNjFqYk\ +lSSEpQbW9VQXpkWHcyL0d0Vk9JUTFwUFJnOXNYN0JaMmUyV1YKdmd5aThPUEJxRWtwblBiMkU5Z0d1U\ +25rY01OeWFVbUl0c3pFandadW14dnZrUW5HanlTb3pjY2R3dnNQNnBJagpoN0g5VUNQTHdOM0N5N1lp\ +UmliSlZBWlNjZkF6QzhubXNLODQrTzJUZHBzTXk0SEZDMmM4dlZiclpteTVkWC9qCk1ESnBzS25JWlZ\ +JMmpXSzRpRS82aUdIWVdoY3JvWnYvVEJ0YW1SQUxTWDVOYkhhWTI0bXVRSG5yMldtaytld3EKbHRGbC\ +90bXgyVkpWUitMZ0JCUGhBb0lCQVFENEI0MjQyVTgvbkJ4d2RzelhCdWxBOVFTa0I5Zktud0RlRkV3S\ +wp3Nks0eU14YVdRU04ycjRxRDhLcW93OVZVMzRYdkRWbFh0RUlDaVh3Q0hZdW5IL3g3cXNSdEVzbHJM\ +aWg4UHRPClpDSU8zUml4RmlIQXFlQUh2YXF0NVhXdndaREx6WnV2THRJOTdINkZ1QjYxck5qMnhxdlR\ +IN05pUmp2S0R0WXgKR1VtNURoQ094cm9tR0NkWHRnWHJGaS9WRU1TSmpQMkM3OTNrN1pTNmNZL0Nkc1\ +RqWEsrZ1UzeWM3OC9kN1pYbwpKMGg2WFlSdmhlQW9Bd2dkVTl4MWtYL2ZmY2tZK1hUcFRwV2xXWmtlU\ +ytsejBpQkRnUlJzMm45OFZDeTZDRmRZCldsZXZaZy9SWXZ6dzlKdWFVcXArOHpHbHNXR2xuOEhtZW5X\ +Q1luSHJnNFBxQnRkL0FvSUJBUUQrOXhDL1N5ZGIKVWZxMUJHYy95YktZc3RLb3A1azB0d3M0SlUwTzN\ +aT1U3MlZ5YTdLT2lTemdPSzVnL2QzckhMMXR2dHViTDBWNAo1dEF4a1AzSkVYbmxZZU5IVkpROTc2RH\ +NGS3Ztc0FGL2JJdVBsdFBRT1dyM3g1eW1RU3lCOTBUczV0dFdWMTVQCktYYVNnMTZidDhwNS9MeERkZ\ +ng2c1YzN1o5RDFRd21EQllreVFIcWQ4clljTm9ad1M5ZnI3UTZhN1ZNSDVtT3IKbEF5dzBCYVdZQk9k\ +bjFGd0pVV3NlRlpmTy9vNUVqZk9Hd0xMR1hiOEVmQ1hqdlRYcUNHLzNrT1JvN1NkOWY1eQowVjIxMmt\ +YVVNINHNDbFB0SmwzeHpaMWJxQ2RMVDNITWNLUTk5UGFFVFppQnNXQ1lOcXg1c0Q0RGhoMzdZQ2hKCm\ +hlN3VUM1E0MElINUFvSUJBREN1VXR1b0UweloyQjhld2grbUpKdnlPMEh5cENFSnlrTE1Xd3gxejNkV\ +E9nQzEKbmhZMWk4TjNxbTZSYUk0SHdDVHFkTlI3b3ExZ1NJZnZNVHIrem9IdXBUYnBXeUorM3hJeDJU\ +Rk9wL3lnMnByUApURHFqWE94SUJycnc0WU5vaTRIa3poeTVKTnl3a1RpdnBaOWsySVMvQTdTQmNWVGx\ +raENianVDK0pPRWthSTJOClpiWGFZY1p1WElVQ3FzcTM2c3RRbCtWZUxRQWt2VjlHc0wrclRnT09Dbz\ +UrTkdRZEVZQnVoRkMzZlJzL1JhSVoKOWFBRTBFL3BTTWp1a05tTnQ2Mm1NSk1tTUdydXhnWFRRblBRR\ +npNSW43aXB2Z0hxQjRsUDM4emdsbnMvbmZVcgo1NWRuZXk3ejhMRFFETHVIc0RHd3hINzNKQjgrTVR2\ +WGFVbkNwQU1DZ2dFQVNBSGxBL0dvdXR6TFRvWmcxcDRUClI1YnhjZHBycFh5d3VYbW5hclJmY3VldG9\ +nUVNtTGpiS0xRNVk0RXZSTENJTzA5MDNENGNnOG5FTU10L01XTXoKSnZwZll3emJGU2J4THR1anRQSX\ +VhaHR3eXV2UkJIVEM1aG5FL3h0WEE1bWZLTDBHWXpzbmtubm1WL2lzSnBSZwpwZFVnSW5sWEJodkRyR\ +FlreUsvWEp0N1FZWlhlUzI5NXlUd0krZndoamlzVVBlTWEyUmRUUE9rQ01JbUVaNUhZCjJHSmZjS25H\ +SkxDVHpDKzNPcGtQazdFRE4vTUlMS2F3YVUxaGp1cVlKWVVUVmpXQzFEM2VUL1ViWHptM0VQNHMKVEN\ +uYWpCYVMzN0N2YVd4ek5JektXZS9TSXdGbEFmYWNSTHlneUR4Z3Q3bHp1akVObEtvU2xya3h3ckpEND\ +Z2WAptUUtDQVFBcTVQWWxSQjgvNnFiWWt1OTA0NUZRdXk2QWtlYXBaMW0raS9SQzZtbFRvUXB6NDlPU\ +Gs4ZGx5YjVtCndvSVhpaEo2V05jN1RsWlRYMnpQTTRBS0M5VFNBUWJrWDg5bjEyU2VDSUlHbXVINnk0\ +TjZiY2lxZjVVcSsvc0IKcHJKeFRNYlRSUFpqS0VVd1N0SFg1MUQ1bi9sQnZERGY3Y2VEZytsYlE0RjR\ +KMTlPd09oZ1lGcjFheGQvNXd2VgpURjNoVlQwbFZGN2RyRC9iMHZOcmxnbUNjbEk4UDg1a2dkRUhZbG\ +ZtTFoxeXJIMkNXVy9SS0lsWk9ZdFVuNFNpCkp5a2VlNDROWElXU3ovalRBdFRta3VQTzRvUjF5d3dRc\ +jdhUTF5a3hRVm9rVm5vY2xqU0tyQlk4R294a0I0eDIKUDNrb3F1UnkvcUd3QzBnN1o4ZjBTQjNQZVZt\ +eQotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==") + ("ringAccountCert" . "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ\ +0F3SUJBZ0lJRm1tNmZuaWRndEl3RFFZSktvWklodmNOQVFFTUJRQXdUREVRTUE0R0ExVUUKQXhNSFNt\ +RnRhU0JEUVRFNE1EWUdDZ21TSm9tVDhpeGtBUUVUS0RjNVpqSTJaVFZpWTJNeU9EWXlPREppT0dFMwp\ +PRFF6TUdOak1EWXpNakV4T1RFNFkyWm1PVGd3SGhjTk1qRXdOREUyTVRjek1qRXdXaGNOTXpFd05ERT\ +BNVGN6Ck1qRXdXakJKTVEwd0N3WURWUVFERXdSS1lXMXBNVGd3TmdZS0NaSW1pWlB5TEdRQkFSTW9aa\ +k16TkRWbU1qYzMKTldSa1ptVXdOMkUwWWpCa09UVmtZV1ZoTVRFeFpERTFabUpqTVRFNU9UQ0NBaUl3\ +RFFZSktvWklodmNOQVFFQgpCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFQY0czaUQwUk84NE5XL2ZGVllWNHB\ +LQ2MyZE1EQXFCZVN2NEpCcTQ3OHQ5ClhjeERPeEhqSnYvR0dLRnFoVyt4cWFDOUJKZG44TGthdjBBTn\ +MvV1d3YXRNSDVsYlQrZlc0T2pGOVNCZ1Ivc2UKUlNPdWE1QncrSVJlSk5VSnR3enBJS3k1S1RhMnEwd\ +0RyUXhObXBuci9UK2NEK2RQSTJveUlFd2xwRHNKT2lVMAp6eStDU2pBb0FTYkh5TWcraGQwcWJiRFBU\ +ck1uR2xKYkZubXdBQTBaRWU3aWg5Ymx0dlFPaU9TQWJVQm9IUHFpCmNtQW41RVFoNjVFa3dCb1p1U3h\ +qanlCVzZpWUJUbmc5S2c4U2pPVk9BYkJqaXZFZ3RyaFpYZ1JVdE11UWFmaDcKcjRRWDRuYjhRTUE5WX\ +lhME8xbFBJTUd5VEtiNHFIaE02VTd1RHV0NUFjcTN1NGFaZElPQ2prbUNFM3l4UU5UWAo5Vzl0b1NvV\ +1l2YVI1d1gxaFI5eVZCMFZ4V0N0djdZZklha1JiaWkxMS8xZHp3M1dXU1dMbTArTU4rY0pUdnluCk1E\ +U29GNjRpRmcycTVYd1VtWXpPRmJKQUtVd3M4QmVhcFRCVnRYaldDZmNKM01xZEZScDZvM25Lenlja0Y\ +wamoKUjdLSmNRaWhJUW1sbXM1WEtzeHV1d0VMMmFKQ0o0Qm9uSUJDaXZWa09LbllsamtDeVNObzkzam\ +lMalJ4M0ZtRwpoTnJyT3lNbjFIcEd0Z1J2eFUxOC9Ob3FFaWtpdEhCeUtYNW5XU1lUUWN4MXlZY3kxc\ +Vo5cjRqTmdQRzNuUHg0CjJEN2tPTlJPWXNVYXNBUlVTZHJqdlc5aXB2TDAvN2s1Yi9sRzh1WThPODN0\ +NUFPSTFVUG85Y05qMzNyeWJwbUgKQWdNQkFBR2pRekJCTUIwR0ExVWREZ1FXQkJUek5GOG5kZDMrQjZ\ +TdzJWMnVvUkhSWDd3Um1UQVBCZ05WSFJNQgpBZjhFQlRBREFRSC9NQThHQTFVZER3RUIvd1FGQXdNSE\ +JnQXdEUVlKS29aSWh2Y05BUUVNQlFBRGdnSUJBRDMrCjlscFluMjZyeG5pekY2UkNvZFFFVmgvOVF2R\ +Wp1V3dHUWZqa3gxb3VlVjdDMzUyWnpIT2hWU3VGcG43TUxkVFcKamI2dWhMRkpoMWtlTDlYZ0pHalMy\ +V0UwdDlJUGp2UWx5UHIwRWJoWGRJNDJMYUR0NDk0dWQ2MEE0bWg0bW1zbwovcjY2NERKOWMwUjZBOUd\ +QZUJjWi9zekhMQjJ4VmM5M3hYNjVjcmNoVTFLMDBVK2ZZUWtHS0xidFFXeUZzdnlKCmRHdFRxamVlYV\ +VNeGt5dFFWNWFxVHJ1SG4vV3U2RWRZejYrZ0ViQXJHUURRK1J3QmMxMDNGcWRIU0xReWdEWHoKd0pNV\ +GhDREdBVmR6V2NCemJYL1JIZms5bTZzK01HblNJUFBrNG9FOUdFbGdQc2JUZ2FrTzc3aElUdzRxZXJi\ +TAp2M0tiaTVOeVZZVFNVa3hDb0VRSkIrWlZJT3BVNFhRdVAvNkZkWldBRk1LU0Jkd1JLcnRmT1hZT1E\ +xcVBvYU1uCjZPR1VGMU0rYWZ0dTNCMkNhZ2ZMaE5hbVBoSjdxWTNSMzJhK2VRVllvYWlESXZKMElIUH\ +FnQ0NuWXlIaVBHTjQKS3VvaE9IZFpXTkxOUXdIQTg3SDV5NEwrSGZISmdFZFppdWYrbTNMa3JJcnN5M\ +WFoUjgyaEpZVkhreHVyZVRDcQpIR2NJaUIvTHpkSG91WFVrWGNrNjRvWXVRQnM3ZE9KVEx6bGlibU8x\ +elRzVWRoc0d4dE9zc2lYWkFjUURmZHBnCjNHdFJ3UkRwd0ZtL2k5TC9UWjMzMjBwY0VZd21aN1dBSzJ\ +Ra3Z6eWZlelRGeXdLWmh3c0RQRkwycllzLzhXdWsKRk1sek5BVmRMTytTS1RxeGlkM0l1elFaQ0FwU0\ +5ybTJMY2ZVN2grWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBV\ +EUtLS0tLQpNSUlGWFRDQ0EwV2dBd0lCQWdJSXV0T3MxUXBsSVRvd0RRWUpLb1pJaHZjTkFRRU1CUUF3\ +VERFUU1BNEdBMVVFCkF4TUhTbUZ0YVNCRFFURTRNRFlHQ2dtU0pvbVQ4aXhrQVFFVEtEYzVaakkyWlR\ +WaVkyTXlPRFl5T0RKaU9HRTMKT0RRek1HTmpNRFl6TWpFeE9URTRZMlptT1Rnd0hoY05NakV3TkRFMk\ +1UY3pNakExV2hjTk16RXdOREUwTVRjegpNakExV2pCTU1SQXdEZ1lEVlFRREV3ZEtZVzFwSUVOQk1UZ\ +3dOZ1lLQ1pJbWlaUHlMR1FCQVJNb056bG1NalpsCk5XSmpZekk0TmpJNE1tSTRZVGM0TkRNd1kyTXdO\ +ak15TVRFNU1UaGpabVk1T0RDQ0FpSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0l\ +CQUxTcGduY2tYODd3OE5vVzdRWE5DbWtHSHphVnN1NE9UbjFSSUhqVQpGR0w5MjNvVlk5RHVoZjFtVn\ +llamgremVCOCszaTR4b1E5MU04WDdTVnk4SHZxejQ4c0I2djAxMk5RTGpwN08xCkd4dmc1a0REeDRqb\ +mVyUWd6d0JzVEFuaC8xNy9SOEFDZkhINzYzUjhMRjY1clJPSStOT21KNUV2cU9rRWJJOXYKb0UySko1\ +czBVSHZZM1hTQ2pid01ROXRkM2c0SG9LNmRYSU44a2d6ejg0Yi83YkF5TnZ4a09rNnVSei9CSjhhZQo\ +zWjZudElza2tmVHhIZFpBbGtpVW5DU21Bb3VwQ01TbGRLS3RMRzNja2FhN0FDMDRxS29Rekc0WTN2Tl\ +RCOUlmCmdvYThOazlKMThoaFN2REVuR0haaURLR1dkQVA5RW5GRHZERnRFRFZyVVZSWUN5WHdyejMya\ +29Na3dYODVUMTgKc1F2TUZSdWRsa1BoOGxmbnFEWTFpZEd1R2IvSGlnU1ZqalJKZlVTbS9lZ3hycStS\ +TnpJWGFBOWNuOXAxQkdFbwpZU0YxeUdtTEgzSkJscUhVRTg4VnFkenZRdXZ3Mkhvakdvd0pkdlhmU2l\ +oZk5WOTh6VkEzQ2o3L0d3V2w0VU9OCkRUVUk2SzUrR3Y0a01iYXdxcWduaFlHbHBlZVp2bHE2UlI3OV\ +pNblRWVjJ1RWo4aERYQzdFTGN2Yk5tekJSSkoKS0gzaUxIbmNlSkxOV2NIMUV0K0pkaEFRR2xEYmk2R\ +VR0dHRuaWVaN3ZkMyt6UE00eXpkb3JEMlJXUjRyWG1acQpGRlI5eTBab2hQVTBjVmdKZHFtdUpyV01Z\ +eTVsOGt5SWdwaExkdTR1WWRpc0RPMG1ianEySE55K3pEakpyYXFHClNWTkZBZ01CQUFHalF6QkJNQjB\ +HQTFVZERnUVdCQlI1OG01YnpDaGlncmluaERETUJqSVJrWXovbURBUEJnTlYKSFJNQkFmOEVCVEFEQV\ +FIL01BOEdBMVVkRHdFQi93UUZBd01IQmdBd0RRWUpLb1pJaHZjTkFRRU1CUUFEZ2dJQgpBRWFDblluY\ +m9yWmRhWWZRUmxSb0dtSE94T1g5VFdNMXY0dWEweCtFcG11RDVWRDVlWEY1QVZkMlZadEdaeHYvCkd2\ +VnFBU2l0UTk3ampKV2p2bURWTUZtb3hQSmRYWDFkYTd5cmJYeFRmYm1mM1pac1RpdmZVdWQxYThxdUN\ +3dTUKTnBrdHFjV0JyMHRnNzFhOXlidHJOdm1GczZGRE1WMXkrY2JxYlp1UWlDWnc0WmZhekFaeTRQRG\ +NocE9SNjRCSwpBWFZIT05HcWFoV2hwcDkwd2E0TFEwUTE1U3FDR25kYVI4SHg5MHJOeEdkRjM1T1BLU\ +jd1TDdrWDU4ZGxaWStDClJJK1pKMndYMzJUZzgrc1RtTmNaUTliWDdvS0t3R3E5Uk94SjZJSUhOSnN2\ +bXhaSnlPcmE2N21hNTd3OWxiQnUKSzJlQ3cwZjRDeHdLNU1LNStkbWx6R3dhZmJlMG00TTBvVjlhWUx\ +ZTzAwR3Iza05heW9PdjRRVGtEM2pCMzVSMQpDMGJnQmk2eU1sTVJ2akZ5eEZkOFJpL003VG1jcXNObT\ +B5aklqbkZaenVtMFZTR1NLMXlRU3Flak40S0Y2R0JMCllpZ2JpM3c4WG5HYm9pZGdBUE9ncVVJeTJ1d\ +EU1MllzVXFsVHVncXhtM2xDOUhzaDM2UFJLNURDUG93eHVUNlgKcXo1M1ZiN2h6TkxLelpiRlJzbUdF\ +OFY2cWM2bXZTbUFXa25nL3QwaStXVmdGVkZuZFQrQ0oyNTJsa0ZacGljdAp6ekdETW44VUNDRUp4TDR\ +KTklTM2lLOUhlRys2MlZuay9QOEM3YVpLSXpVdjFud25rcVdUUUFYWDBKckJGdDdICjI5ZDk1RElmRT\ +RuT0FyS0JFNHc2Z1R4SU1uZzVzWi9ZbDFjcG5wUHlsR3VICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tL\ +S0K") + ("ethKey" . "fN8cOT1lYNziaW0+pjBIgZ8r6+zMMhHsukkWBNPDsFo=") + ("TURN.username" . "ring") + ("TURN.server" . "turn.jami.net") + ("TURN.realm" . "ring") + ("TURN.password" . "ring") + ("TURN.enable" . "true") + ("TLS.verifyServer" . "true") + ("TLS.verifyClient" . "true") + ("TLS.serverName" . "") + ("TLS.requireClientCertificate" . "true") + ("TLS.privateKeyFile" . "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRd0lCQU\ +RBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Mwd2dna3BBZ0VBQW9JQ0FRQzM5b1Z0cXNtUGdaSUgKcHpTV\ +GtlT3BlWC9CSEx2KzFTYnJPSFpVRHEwNFZCUU5BNmJmSFNSWTJpbHE1WEVheXNVSmwzQmsvM0txZEhS\ +cQpEV01wQ1dpcE1Vc2FwSGxJR0tSWHEwbXhQZ29WODZSUVBub1dCRTdhWVVEZTlJZXlxMmllZXpDK1l\ +YSnBtWTljCk5tblpaMFlHOHJGMEVpWFA0SHpVWGphZklTKzdKTTJ5ZTZyUlpINXBvdHBQNmV4NXhqVU\ +VuNEFFdWhuWGJ6U0EKNnZMaC9wYnhIUVBHMVlmNHFhcU9TY1lXV0ZWaE5ySVBCVFJNZ2RaTWRGRTh0W\ +VFyaUNkNTV4dUhrYncrQmY1VQpmVENqN25tVEcybDJNbGcrSXBHVkFXUFRWNkl0NFNiS3VadW5MZmRD\ +S2FrSU03SnA3V2dJWjZNRHdkaFFQWjJNClJPQzN0b0c1ajh4aFZOU0Q5QlZiOGZBcnRmeHdldlliQk1\ +yOEdRV2JQYlRKYk9tZHZnUGRGcXNTb1F4QnRUcWQKSG1TMG53RjJ4UGZrdm1Cd3BSWWhHNHpuZlVtMl\ +BQcDBIK1g5Sk96d0R1QWI5K0c0cWZtK3Z6bDd1N0o3anJ0TQpHQlJlSlREdGFNd2RHa0lmcUNRZGREZ\ +HFpM1hzVHVVOVNubVN4NkdxT2V6ZU04bk55dmtZWjR2Uk9vODFzU1JjCjQ4L3pZZjJ1OFBJUm4vVy9x\ +Zk9hbjVIbkcxeFNBUDZkcHAzQ1Y0YTgwUFduZyszcXQ4NW5Ha2k2Nk00NUlnRjgKNHZwRGUyM2YyZnp\ +rTk5OOTk1VTBvUStKVGg5eG1HaWJmenlCaXk5d3RUVHVYWDB3Y3gwTm9wNjdYaFFDVVBGSApybVd6Wj\ +V5bXhEdkRhcUN1U3NwcG0vTkRVRWQyc1FJREFRQUJBb0lDQUYyWXo4bzhXdERvMjZPSkx2Ym1BeTcyC\ +jRra2VsWWZTYXpyQ1AzSUZCWnpqS2xCMHl6STVZWVRUZXI4b2ZhTmtCMXdaOE5WeUlxVVhHeVBhSzls\ +MU1BcmwKU1pFRW5iQlEyVXpCbVh6VVU4MVhhUUpxeHpMc2ZqSWR5U09teG1QaFVobFFGRHpJMTYxaXM\ +4MzI0V1A3WjJXaApsU2U1RkFQdjg1TVpYREVhY1c2R0N5SUVTYVMvdkpHQ2loQ2VzL0pCSmpoejdtNT\ +VRU3liSjl0dnJxUE5KSDhJCmhDTm1BUWhEU3NPYVJXNlpBdGV6UEdUb0FQUHNHMXhLMGdwUlVDM1YyR\ +C90bGRRa0VlSjhxaW5TaUN6ZjZIc3cKTnpncjVUbTMzTm96R3Rjc2Z4ZFl0cVB1UzRPRG40bktLSFpE\ +MTBLTng2Qi9HakdQTHIra21jUUVSMUV4U2s2QwpSemtzSTRzRml6VnVLYTFNK2ZqaVNVc3RTdHV1cTJ\ +QQTJJNzFNTTZlMHJmU3ZKM3VESEJYOWY5T1RFaGxZUFBkCnB1VWM2ek1pdEJKRTg2SFo1M08wR0MzdG\ +p1M1BnSnhmWnJyMHBCU20rSzQ3ajVTUFNkaytiVGh3UDZDZDhzWGUKNmljS3YvMXI0SjVqd1pFTmRna\ +2hKeHVuMkE1WVA0c0NBU21JSFFXWmU1cGdFQ0ljUXBHZjNBVllVdVpGeXZ6cApLL3VBRTc0L3NMK0VU\ +UnhDUlhkN0ZJM2cwZFVySTA2WUFIV1FkY1JpdWpIUU12ZXB3V2RrdUdkaE9wV3VRTy9oCjc0MGgvRlZ\ +0dFdOMjc3ajhhc3ptSDA3T2lRWFIwTU1mN2FEaldMaElMYkd2YkV4TzN0TnRBYXZ6cGxmMUNSNk8KUm\ +1nNDdhVTZpR3lON1MwTE85RUJBb0lCQVFEWWMwelh2UmJHTWxkVVZnOFFTT1ZHKzV4NXVocTVyQ2xtQ\ +lp2ZApvYitWS1hkMXBONThraWkxTFBiYUJmZG9JcWVUamdOK0E0YnVpT3RGNjdCWUdISkp0WTk4ZWR5\ +RkpZREtCSzIyCmthRml2eWVuV01UWG5jaHMvUmNXMnVGS1M3cFAyNDRXYkF2clQxZGxmTTF3L1JzbTZ\ +QVjF1dndPSmNJcEdLSzgKWWgyN2hiaThUbEF6bnJCSS9TbGZwNlpxV3Y0MFRLanNNdmVGVFNWRU5yd1\ +F2MVl4TjcxbjA1UWRVUkIrT0lJSQpPenpNMWpNcm43cjNmS3RKTEx0RllEdUhJZzJDL2E0cUZORmpjM\ +296V1laQkhlcDkrWlEyOTgxdy96VTJIZ0oxCkxiajZjMy9qQU5EakI3MnRkcWMxVUkvMWhwVUh2WDJz\ +emY2czM0L3VSY3RlaFgxWEFvSUJBUURaazVaT01RdVMKNzlVQkhCeTRFbFR2cVkvUUxjMUd1aG9LVXE\ +0dzhONEtJZlpMSDk5TTcwek9JWjNoaEdaYWJDREZHN3RqSDl4Vwo3UnhXVWt5cFRDK1h5TTc1YjF0YW\ +9jRVVyZ1hnUnE4R1JFRXg3OUtwWUE3ei95TVc1OHJCTWhHTWlGZ1JTM00zCmNQSzg2dHBvaTFDN2crd\ +lUxcjJwcDEzN0habzZiUHpKTFRPNXA1OG1tTFRPQVpKd1VSZ3pzZWJnc1dsWXZlR0wKZWNBZ3lYbUtt\ +WmVSeGRNUFlCK0NmMno1TWZ5Ujk4MHRVSDJPaitGQjlJNExzRTExREphZ2wwS1lHMWxKRTBFagpodEV\ +1cTFOTG9DcVkzYXVKYjhtRUpqNlA2Ylc2SytCZTBlUi9CS1MraGxxb3VOYWEySnlhSDc4Qis2U1VZWE\ +RuCnd6MityWUhVZEI4M0FvSUJBRFlXc2ZnaloyS0Z4KzdxUm45aVIvRXlCUXNpSjNXSWdSdmVnUEdrY\ +nRTZWRSeXYKNDIwcnRRSjVSd0o2aFRXL216S3pSVW9qSlgvTU5VYld1ODEzNW05bThJRkJqb3F6TVhq\ +S0xJSzM1NlZlY1ZGUApUSGs1RTVHd3VTbGI3dnA2N0FieXJaSUswL3VzYXdHUWEySTF6YWd1aE5BenR\ +yTHVXcE9jZFdZditwQVd2WEJJCi9aKzRvd0xLU0tGL3FvVmZVYkRPQzFSaTlCbWFpcHArTndiVVdYeV\ +pHanFzMDVGejVYUTFPTUZIMUV5M3BqZmIKaFlRODRpeTZBZDQzU3dqY3lKV1lRUUtCQzBZWDRFeWVyW\ +Dd1TTkvaEUxbWRHUGlJdmNwVk8zWCt3Ly9LQndZNQorUGtTd1NKc3lTSDRqTkRsSGE2K2VuNUpSNy81\ +YWVVNENiY0lFcWNDZ2dFQkFLelMvNnhDWnZnalN5V2poK2hxCm4wN3plQW1icUJmTEVZNHJtTFBGVUF\ +ucWFqSElNbDV4SXFnRnFkd05pQ1BCQ2RLbnNaUU9KYjVpZjRUTndKa2wKckJRNzdMUFRVVlJQY2dnVU\ +p4Uzc4S0Rnckl5Vys5V1FPTEIxZEJEb3MzUDhhbFlmb3h5eHV1Wko4SFpCY3BWaQpQQkdHdTFnSDd3V\ +0lyUzBmbVhkWlJQNGp5cGRvM3hFUWNXWEZkK1dCZE9EektmcEcwZkFzZTdDSFdDWnpBdmttCkFYQklH\ +OXQxdGZHNWQvMEZTS05GbTVPb0FPT3h3L0xZNTgrL0RmZXd0U0VBcFdRZkxTL1BmSWxVdUdvQ3FwcEM\ +Kc2pOVXVNSGxxc011Z2JsY29mNHNoZityWjMzQldYOEJSNWdIb21mRE1ibDNDQWp5TXd1dHpybzVxcD\ +BBUTBWWApxOGNDZ2dFQkFLakFXVVRYY1F2TE8yYkxOZmJBTUlSVXk1T2lTZmJCbDNYRThSZnNzaUt2V\ +VBnTFcwSlV1V3FLCjdGdUFxTlJPRHhrS0pMSTdyQlo2YVNqNitFWHpUMnJwY2dFWktnSjFOUEFRdFNs\ +UjNJUkcyU3JJdjBFK3UzbkUKK1laa3pOa2Q0MUJqTkRjRm1HV21lZk5ROUJmaVIrZlZFSkZmcE5oSkl\ +mNUloSWU0RUtZUE5VUXNua0tSVTlxUApzWi9idXBXc2w4bWVFcko3bllJQ05ucHpnSHRpNXdSMlliVF\ +VXT01odmRFUldxMnhTV3BBYmtNMElhZDBUc05kCmUrYVRQVmJOMXFibFZLMm1qUTl2YS9JSkVuSE51V\ +E9TREtJeUpvcVArQkxiRTVjQU5acXQ2OFFadWdOc2RxNHkKV2FoeStydU5LS1F3Mk5MYzQzZUtsNmxv\ +bXdtRlFZOD0KLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=") + ("TLS.password" . "") + ("TLS.negotiationTimeoutSec" . "-1") + ("TLS.method" . "Automatic") + ("TLS.ciphers" . "") + ("TLS.certificateFile" . "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZHVENDQ\ +XdHZ0F3SUJBZ0lJU1pUdlZPQnh3akF3RFFZSktvWklodmNOQVFFTUJRQXdTVEVOTUFzR0ExVUUKQXhN\ +RVNtRnRhVEU0TURZR0NnbVNKb21UOGl4a0FRRVRLR1l6TXpRMVpqSTNOelZrWkdabE1EZGhOR0l3WkR\ +rMQpaR0ZsWVRFeE1XUXhOV1ppWXpFeE9Ua3dIaGNOTWpFd05ERTJNVGN6TWpFd1doY05NekV3TkRFME\ +1UY3pNakV3CldqQlFNUlF3RWdZRFZRUURFd3RLWVcxcElHUmxkbWxqWlRFNE1EWUdDZ21TSm9tVDhpe\ +GtBUUVUS0dFM09XTmwKTURVNE16VmhPRFV5WlRsbU5qWmxOelF3TURjeU5EUXdPVFZpTVdaa1kyVXdO\ +bVV3Z2dJaU1BMEdDU3FHU0liMwpEUUVCQVFVQUE0SUNEd0F3Z2dJS0FvSUNBUUMzOW9WdHFzbVBnWkl\ +IcHpTVGtlT3BlWC9CSEx2KzFTYnJPSFpVCkRxMDRWQlFOQTZiZkhTUlkyaWxxNVhFYXlzVUpsM0JrLz\ +NLcWRIUnFEV01wQ1dpcE1Vc2FwSGxJR0tSWHEwbXgKUGdvVjg2UlFQbm9XQkU3YVlVRGU5SWV5cTJpZ\ +WV6QytZWEpwbVk5Y05tblpaMFlHOHJGMEVpWFA0SHpVWGphZgpJUys3Sk0yeWU2clJaSDVwb3RwUDZl\ +eDV4alVFbjRBRXVoblhielNBNnZMaC9wYnhIUVBHMVlmNHFhcU9TY1lXCldGVmhOcklQQlRSTWdkWk1\ +kRkU4dFlRcmlDZDU1eHVIa2J3K0JmNVVmVENqN25tVEcybDJNbGcrSXBHVkFXUFQKVjZJdDRTYkt1Wn\ +VuTGZkQ0tha0lNN0pwN1dnSVo2TUR3ZGhRUFoyTVJPQzN0b0c1ajh4aFZOU0Q5QlZiOGZBcgp0Znh3Z\ +XZZYkJNcjhHUVdiUGJUSmJPbWR2Z1BkRnFzU29ReEJ0VHFkSG1TMG53RjJ4UGZrdm1Cd3BSWWhHNHpu\ +CmZVbTJQUHAwSCtYOUpPendEdUFiOStHNHFmbSt2emw3dTdKN2pydE1HQlJlSlREdGFNd2RHa0lmcUN\ +RZGREZHEKaTNYc1R1VTlTbm1TeDZHcU9lemVNOG5OeXZrWVo0dlJPbzgxc1NSYzQ4L3pZZjJ1OFBJUm\ +4vVy9xZk9hbjVIbgpHMXhTQVA2ZHBwM0NWNGE4MFBXbmcrM3F0ODVuR2tpNjZNNDVJZ0Y4NHZwRGUyM\ +2YyZnprTk5OOTk1VTBvUStKClRoOXhtR2liZnp5Qml5OXd0VFR1WFgwd2N4ME5vcDY3WGhRQ1VQRkhy\ +bVd6WjV5bXhEdkRhcUN1U3NwcG0vTkQKVUVkMnNRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkRBVUFBNEl\ +DQVFCZmRCc0p4RVVWeFRBeG5vNll1bEFoR1ZvUAplWG8xMHIwOE5DcDZRZlJxeGJlTkZ5aXEvKzEwSE\ +NpL1RoZk41OVdLNlpudFF4NlFNZUxEUVZTb0NjNzlaaWQ4ClE2RUdsWkp1c2RTTmg0VjVteXRCQVZHZ\ +2J2aXJFWU1Wcm5jWWg3bHR2LzVuSGRsbyt2WXV3Vzh0aEhHTk1TUkIKQmhJN2xydmpqY3NkbVl6L1Bp\ +NFNZdkg3c3RaVWpRYmUzNzh3UE90b3lBMTNybEtQUTF4QjJFbUxISDYya21WTQowa25wL1hQQ2o2alR\ +zakpFWko1NzZNMmloYy9DTzkvREVlWWZ4RFlJb004NEF5dTc0UU9UUVN1cnVHRjF5T1RKCmlxRkltTH\ +hMcWRxNk1Zdmx1QjFmTzlKaU9QaS84UEJFTTVpeGYzajlGOFVMQTZUTU1NSklFR1ROeUNzOWpzQloKR\ +UNiWVgweTY4WWtGYnYzQmNWaGk2K1VVWVhBVUd0akhwQ3Z2VThYaHowL0RVZFVaTkpqejRJeWl1ZE5N\ +dnFjQgppSEJDdkxFS1B4VFd1ZlFZaGM5ZkVXTm1valc1WSsvalBVeDcrQWxPM1RXZm1OclBpWDJuSEd\ +5UG5tYjZIR2hWCmp3UU0wT1h4cGxnZ2dQMWpVZ0VYWWRucStjYzdRMy94RFIvdi9Fd244T1d4OVB3eV\ +Y0WFZwZXY5QnpmeWwyQTMKdFhWVkFKMzJtcTlPQmVadXczdWlWU2E2TWdCVG80eXcrYnBjU2VIU2xXO\ +XNRc1NYY3dUNjhvOUdhR2hMNjdXMApwQWx4R2VxSWRtbTZ0MkxIUCtQQnBtL3BweTMvTmh1NFIwMTNv\ +Znp5V2Y0bEcrVnpWVGE0eXVqU1J3UlluYmZyCjlSWER0L0I5VEg5TnNsamdBQT09Ci0tLS0tRU5EIEN\ +FUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ0F3SU\ +JBZ0lJRm1tNmZuaWRndEl3RFFZSktvWklodmNOQVFFTUJRQXdUREVRTUE0R0ExVUUKQXhNSFNtRnRhU\ +0JEUVRFNE1EWUdDZ21TSm9tVDhpeGtBUUVUS0RjNVpqSTJaVFZpWTJNeU9EWXlPREppT0dFMwpPRFF6\ +TUdOak1EWXpNakV4T1RFNFkyWm1PVGd3SGhjTk1qRXdOREUyTVRjek1qRXdXaGNOTXpFd05ERTBNVGN\ +6Ck1qRXdXakJKTVEwd0N3WURWUVFERXdSS1lXMXBNVGd3TmdZS0NaSW1pWlB5TEdRQkFSTW9aak16Tk\ +RWbU1qYzMKTldSa1ptVXdOMkUwWWpCa09UVmtZV1ZoTVRFeFpERTFabUpqTVRFNU9UQ0NBaUl3RFFZS\ +ktvWklodmNOQVFFQgpCUUFEZ2dJUEFEQ0NBZ29DZ2dJQkFQY0czaUQwUk84NE5XL2ZGVllWNHBLQ2My\ +ZE1EQXFCZVN2NEpCcTQ3OHQ5ClhjeERPeEhqSnYvR0dLRnFoVyt4cWFDOUJKZG44TGthdjBBTnMvV1d\ +3YXRNSDVsYlQrZlc0T2pGOVNCZ1Ivc2UKUlNPdWE1QncrSVJlSk5VSnR3enBJS3k1S1RhMnEwd0RyUX\ +hObXBuci9UK2NEK2RQSTJveUlFd2xwRHNKT2lVMAp6eStDU2pBb0FTYkh5TWcraGQwcWJiRFBUck1uR\ +2xKYkZubXdBQTBaRWU3aWg5Ymx0dlFPaU9TQWJVQm9IUHFpCmNtQW41RVFoNjVFa3dCb1p1U3hqanlC\ +VzZpWUJUbmc5S2c4U2pPVk9BYkJqaXZFZ3RyaFpYZ1JVdE11UWFmaDcKcjRRWDRuYjhRTUE5WXlhME8\ +xbFBJTUd5VEtiNHFIaE02VTd1RHV0NUFjcTN1NGFaZElPQ2prbUNFM3l4UU5UWAo5Vzl0b1NvV1l2YV\ +I1d1gxaFI5eVZCMFZ4V0N0djdZZklha1JiaWkxMS8xZHp3M1dXU1dMbTArTU4rY0pUdnluCk1EU29GN\ +jRpRmcycTVYd1VtWXpPRmJKQUtVd3M4QmVhcFRCVnRYaldDZmNKM01xZEZScDZvM25Lenlja0YwamoK\ +UjdLSmNRaWhJUW1sbXM1WEtzeHV1d0VMMmFKQ0o0Qm9uSUJDaXZWa09LbllsamtDeVNObzkzamlMalJ\ +4M0ZtRwpoTnJyT3lNbjFIcEd0Z1J2eFUxOC9Ob3FFaWtpdEhCeUtYNW5XU1lUUWN4MXlZY3kxcVo5cj\ +RqTmdQRzNuUHg0CjJEN2tPTlJPWXNVYXNBUlVTZHJqdlc5aXB2TDAvN2s1Yi9sRzh1WThPODN0NUFPS\ +TFVUG85Y05qMzNyeWJwbUgKQWdNQkFBR2pRekJCTUIwR0ExVWREZ1FXQkJUek5GOG5kZDMrQjZTdzJW\ +MnVvUkhSWDd3Um1UQVBCZ05WSFJNQgpBZjhFQlRBREFRSC9NQThHQTFVZER3RUIvd1FGQXdNSEJnQXd\ +EUVlKS29aSWh2Y05BUUVNQlFBRGdnSUJBRDMrCjlscFluMjZyeG5pekY2UkNvZFFFVmgvOVF2RWp1V3\ +dHUWZqa3gxb3VlVjdDMzUyWnpIT2hWU3VGcG43TUxkVFcKamI2dWhMRkpoMWtlTDlYZ0pHalMyV0Uwd\ +DlJUGp2UWx5UHIwRWJoWGRJNDJMYUR0NDk0dWQ2MEE0bWg0bW1zbwovcjY2NERKOWMwUjZBOUdQZUJj\ +Wi9zekhMQjJ4VmM5M3hYNjVjcmNoVTFLMDBVK2ZZUWtHS0xidFFXeUZzdnlKCmRHdFRxamVlYVVNeGt\ +5dFFWNWFxVHJ1SG4vV3U2RWRZejYrZ0ViQXJHUURRK1J3QmMxMDNGcWRIU0xReWdEWHoKd0pNVGhDRE\ +dBVmR6V2NCemJYL1JIZms5bTZzK01HblNJUFBrNG9FOUdFbGdQc2JUZ2FrTzc3aElUdzRxZXJiTAp2M\ +0tiaTVOeVZZVFNVa3hDb0VRSkIrWlZJT3BVNFhRdVAvNkZkWldBRk1LU0Jkd1JLcnRmT1hZT1ExcVBv\ +YU1uCjZPR1VGMU0rYWZ0dTNCMkNhZ2ZMaE5hbVBoSjdxWTNSMzJhK2VRVllvYWlESXZKMElIUHFnQ0N\ +uWXlIaVBHTjQKS3VvaE9IZFpXTkxOUXdIQTg3SDV5NEwrSGZISmdFZFppdWYrbTNMa3JJcnN5MWFoUj\ +gyaEpZVkhreHVyZVRDcQpIR2NJaUIvTHpkSG91WFVrWGNrNjRvWXVRQnM3ZE9KVEx6bGlibU8xelRzV\ +WRoc0d4dE9zc2lYWkFjUURmZHBnCjNHdFJ3UkRwd0ZtL2k5TC9UWjMzMjBwY0VZd21aN1dBSzJRa3Z6\ +eWZlelRGeXdLWmh3c0RQRkwycllzLzhXdWsKRk1sek5BVmRMTytTS1RxeGlkM0l1elFaQ0FwU05ybTJ\ +MY2ZVN2grWAotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS\ +0tLQpNSUlGWFRDQ0EwV2dBd0lCQWdJSXV0T3MxUXBsSVRvd0RRWUpLb1pJaHZjTkFRRU1CUUF3VERFU\ +U1BNEdBMVVFCkF4TUhTbUZ0YVNCRFFURTRNRFlHQ2dtU0pvbVQ4aXhrQVFFVEtEYzVaakkyWlRWaVky\ +TXlPRFl5T0RKaU9HRTMKT0RRek1HTmpNRFl6TWpFeE9URTRZMlptT1Rnd0hoY05NakV3TkRFMk1UY3p\ +NakExV2hjTk16RXdOREUwTVRjegpNakExV2pCTU1SQXdEZ1lEVlFRREV3ZEtZVzFwSUVOQk1UZ3dOZ1\ +lLQ1pJbWlaUHlMR1FCQVJNb056bG1NalpsCk5XSmpZekk0TmpJNE1tSTRZVGM0TkRNd1kyTXdOak15T\ +VRFNU1UaGpabVk1T0RDQ0FpSXdEUVlKS29aSWh2Y04KQVFFQkJRQURnZ0lQQURDQ0Fnb0NnZ0lCQUxT\ +cGduY2tYODd3OE5vVzdRWE5DbWtHSHphVnN1NE9UbjFSSUhqVQpGR0w5MjNvVlk5RHVoZjFtVnllamg\ +remVCOCszaTR4b1E5MU04WDdTVnk4SHZxejQ4c0I2djAxMk5RTGpwN08xCkd4dmc1a0REeDRqbmVyUW\ +d6d0JzVEFuaC8xNy9SOEFDZkhINzYzUjhMRjY1clJPSStOT21KNUV2cU9rRWJJOXYKb0UySko1czBVS\ +HZZM1hTQ2pid01ROXRkM2c0SG9LNmRYSU44a2d6ejg0Yi83YkF5TnZ4a09rNnVSei9CSjhhZQozWjZu\ +dElza2tmVHhIZFpBbGtpVW5DU21Bb3VwQ01TbGRLS3RMRzNja2FhN0FDMDRxS29Rekc0WTN2TlRCOUl\ +mCmdvYThOazlKMThoaFN2REVuR0haaURLR1dkQVA5RW5GRHZERnRFRFZyVVZSWUN5WHdyejMya29Na3\ +dYODVUMTgKc1F2TUZSdWRsa1BoOGxmbnFEWTFpZEd1R2IvSGlnU1ZqalJKZlVTbS9lZ3hycStSTnpJW\ +GFBOWNuOXAxQkdFbwpZU0YxeUdtTEgzSkJscUhVRTg4VnFkenZRdXZ3Mkhvakdvd0pkdlhmU2loZk5W\ +OTh6VkEzQ2o3L0d3V2w0VU9OCkRUVUk2SzUrR3Y0a01iYXdxcWduaFlHbHBlZVp2bHE2UlI3OVpNblR\ +WVjJ1RWo4aERYQzdFTGN2Yk5tekJSSkoKS0gzaUxIbmNlSkxOV2NIMUV0K0pkaEFRR2xEYmk2RVR0dH\ +RuaWVaN3ZkMyt6UE00eXpkb3JEMlJXUjRyWG1acQpGRlI5eTBab2hQVTBjVmdKZHFtdUpyV01ZeTVsO\ +Gt5SWdwaExkdTR1WWRpc0RPMG1ianEySE55K3pEakpyYXFHClNWTkZBZ01CQUFHalF6QkJNQjBHQTFV\ +ZERnUVdCQlI1OG01YnpDaGlncmluaERETUJqSVJrWXovbURBUEJnTlYKSFJNQkFmOEVCVEFEQVFIL01\ +BOEdBMVVkRHdFQi93UUZBd01IQmdBd0RRWUpLb1pJaHZjTkFRRU1CUUFEZ2dJQgpBRWFDblluYm9yWm\ +RhWWZRUmxSb0dtSE94T1g5VFdNMXY0dWEweCtFcG11RDVWRDVlWEY1QVZkMlZadEdaeHYvCkd2VnFBU\ +2l0UTk3ampKV2p2bURWTUZtb3hQSmRYWDFkYTd5cmJYeFRmYm1mM1pac1RpdmZVdWQxYThxdUN3dTUK\ +TnBrdHFjV0JyMHRnNzFhOXlidHJOdm1GczZGRE1WMXkrY2JxYlp1UWlDWnc0WmZhekFaeTRQRGNocE9\ +SNjRCSwpBWFZIT05HcWFoV2hwcDkwd2E0TFEwUTE1U3FDR25kYVI4SHg5MHJOeEdkRjM1T1BLUjd1TD\ +drWDU4ZGxaWStDClJJK1pKMndYMzJUZzgrc1RtTmNaUTliWDdvS0t3R3E5Uk94SjZJSUhOSnN2bXhaS\ +nlPcmE2N21hNTd3OWxiQnUKSzJlQ3cwZjRDeHdLNU1LNStkbWx6R3dhZmJlMG00TTBvVjlhWUxZTzAw\ +R3Iza05heW9PdjRRVGtEM2pCMzVSMQpDMGJnQmk2eU1sTVJ2akZ5eEZkOFJpL003VG1jcXNObTB5akl\ +qbkZaenVtMFZTR1NLMXlRU3Flak40S0Y2R0JMCllpZ2JpM3c4WG5HYm9pZGdBUE9ncVVJeTJ1dEU1Ml\ +lzVXFsVHVncXhtM2xDOUhzaDM2UFJLNURDUG93eHVUNlgKcXo1M1ZiN2h6TkxLelpiRlJzbUdFOFY2c\ +WM2bXZTbUFXa25nL3QwaStXVmdGVkZuZFQrQ0oyNTJsa0ZacGljdAp6ekdETW44VUNDRUp4TDRKTklT\ +M2lLOUhlRys2MlZuay9QOEM3YVpLSXpVdjFud25rcVdUUUFYWDBKckJGdDdICjI5ZDk1RElmRTRuT0F\ +yS0JFNHc2Z1R4SU1uZzVzWi9ZbDFjcG5wUHlsR3VICi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K") + ("STUN.server" . "") + ("STUN.enable" . "false") + ("SRTP.rtpFallback" . "false") + ("SRTP.keyExchange" . "sdes") + ("SRTP.enable" . "true") + ("RingNS.uri" . "") + ("RingNS.account" . "0790738ce15fa05933b49dd77034312787da86c3") + ("DHT.PublicInCalls" . "true") + ("Account.videoPortMin" . "49152") + ("Account.videoPortMax" . "65534") + ("Account.videoEnabled" . "true") + ("Account.username" . "f3345f2775ddfe07a4b0d95daea111d15fbc1199") + ("Account.useragent" . "") + ("Account.upnpEnabled" . "true") + ("Account.type" . "RING") + ("Account.ringtoneEnabled" . "true") + ("Account.rendezVous" . "true") + ("Account.publishedSameAsLocal" . "true") + ("Account.publishedPort" . "5060") + ("Account.publishedAddress" . "") + ("Account.presenceSubscribeSupported" . "true") + ("Account.peerDiscovery" . "false") + ("Account.managerUsername" . "") + ("Account.managerUri" . "") + ("Account.mailbox" . "") + ("Account.localModeratorsEnabled" . "true") + ("Account.localInterface" . "default") + ("Account.hostname" . "bootstrap.jami.net") + ("Account.hasCustomUserAgent" . "false") + ("Account.enable" . "true") + ("Account.dtmfType" . "overrtp") + ("Account.displayName" . "dummy") + ("Account.defaultModerators" . "") + ("Account.audioPortMin" . "16384") + ("Account.audioPortMax" . "32766") + ("Account.archiveHasPassword" . "false") + ("Account.allowCertFromTrusted" . "true") + ("Account.allowCertFromHistory" . "true") + ("Account.allowCertFromContact" . "true") + ("Account.allModeratorEnabled" . "true") + ("Account.alias" . "dummy") + ("Account.activeCallLimit" . "-1") + ("Account.accountPublish" . "false") + ("Account.accountDiscovery" . "false"))) diff --git a/gnu/tests/telephony.scm b/gnu/tests/telephony.scm new file mode 100644 index 0000000000..1155a9dbc2 --- /dev/null +++ b/gnu/tests/telephony.scm @@ -0,0 +1,366 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Maxim Cournoyer . +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix 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 General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (gnu tests telephony) + #:use-module (gnu) + #:use-module (gnu packages) + #:use-module (gnu packages guile) + #:use-module (gnu tests) + #:use-module (gnu system vm) + #:use-module (gnu services) + #:use-module (gnu services dbus) + #:use-module (gnu services networking) + #:use-module (gnu services ssh) + #:use-module (gnu services telephony) + #:use-module (guix gexp) + #:use-module (guix modules) + #:export (%test-jami + %test-jami-provisioning)) + +;;; +;;; Jami daemon. +;;; + +(include "data/jami-dummy-account.dat") ;defines %jami-account-content-sexp + +(define %dummy-jami-account-archive + ;; A Jami account archive is a gzipped JSON file. + (computed-file + "dummy-jami-account.gz" + (with-extensions (list guile-json-4 guile-zlib) + #~(begin + (use-modules (json) (zlib)) + (let ((port (open-output-file #$output))) + (call-with-gzip-output-port port + (lambda (port) + (scm->json '#$%jami-account-content-sexp port)))))))) + +(define %allowed-contacts '("1dbcb0f5f37324228235564b79f2b9737e9a008f" + "2dbcb0f5f37324228235564b79f2b9737e9a008f")) + +(define %moderators '("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")) + +(define %dummy-jami-account (jami-account + (archive %dummy-jami-account-archive) + (allowed-contacts %allowed-contacts) + (moderators %moderators) + (rendezvous-point? #t) + (peer-discovery? #f) + (bootstrap-hostnames '("bootstrap.me" + "fallback.another.host")) + (name-server-uri "https://my.name.server"))) + +(define* (make-jami-os #:key provisioning?) + (operating-system + (host-name "jami") + (timezone "America/Montreal") + (locale "en_US.UTF-8") + + (bootloader (bootloader-configuration + (bootloader grub-bootloader) + (target "/dev/sdX"))) + (file-systems (cons (file-system + (device (file-system-label "my-root")) + (mount-point "/") + (type "ext4")) + %base-file-systems)) + (firmware '()) + + (services (cons* (service jami-service-type + (if provisioning? + (jami-configuration + (debug? #t) + (accounts (list %dummy-jami-account))) + (jami-configuration + (debug? #t)))) + (service dbus-root-service-type) + ;; The following services/packages are added for + ;; debugging purposes. + (service dhcp-client-service-type) + (service openssh-service-type + (openssh-configuration + (permit-root-login #t) + (allow-empty-passwords? #t))) + %base-services)) + (packages (cons* (specification->package "recutils") + (specification->package "strace") + %base-packages)))) + +(define %jami-os + (make-jami-os)) + +(define %jami-os-provisioning + (make-jami-os #:provisioning? #t)) + +(define* (run-jami-test #:key provisioning?) + "Run tests in %JAMI-OS. When PROVISIONING? is true, test the +accounts provisioning feature of the service." + (define os (marionette-operating-system + (if provisioning? + %jami-os-provisioning + %jami-os) + #:imported-modules '((gnu services herd) + (guix combinators)))) + (define vm (virtual-machine + (operating-system os) + (memory-size 512))) + + (define username (assoc-ref %jami-account-content-sexp + "Account.username")) + + (define test + (with-imported-modules (source-module-closure + '((gnu build marionette) + (gnu build jami-service))) + #~(begin + (use-modules (rnrs base) + (srfi srfi-11) + (srfi srfi-64) + (gnu build marionette) + (gnu build jami-service)) + + (define marionette + (make-marionette (list #$vm))) + + (mkdir #$output) + (chdir #$output) + + (test-begin "jami") + + (test-assert "service is running" + (marionette-eval + '(begin + (use-modules (gnu services herd)) + (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) (number? pid)))))) + marionette)) + + (test-assert "service can be stopped" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + (setenv "PATH" "/run/current-system/profile/bin") + (let ((pid (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) pid)))))) + + (assert (number? pid)) + + (match (stop-service 'jami) + (services ;a list of service symbols + (member 'jami services))) + ;; Sometimes, the process still appear in pgrep, even + ;; though we are using waitpid after sending it SIGTERM + ;; in the service; use retries. + (with-retries 20 1 + (not (zero? (status:exit-val + (system* "pgrep" "dring"))))))) + marionette)) + + (test-assert "service can be restarted" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + ;; Start and retrieve the current PID. + (define pid (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) pid))))) + (assert (number? pid)) + + ;; Restart the service. + (restart-service 'jami) + + (define new-pid (match (start-service 'jami) + (#f #f) + (('service response-parts ...) + (match (assq-ref response-parts 'running) + ((pid) pid))))) + (assert (number? new-pid)) + + (not (eq? pid new-pid))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami accounts provisioning, account present" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + ;; Accounts take some time to appear after being added. + (with-retries 20 1 + (with-shepherd-action 'jami ('list-accounts) results + (let ((account (assoc-ref (car results) #$username))) + (assert (string=? #$username + (assoc-ref account + "Account.username"))))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami accounts provisioning, allowed-contacts" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + ;; Public mode is disabled. + (with-shepherd-action 'jami ('list-account-details) + results + (let ((account (assoc-ref (car results) #$username))) + (assert (string=? "false" + (assoc-ref account + "DHT.PublicInCalls"))))) + + ;; Allowed contacts match those declared in the configuration. + (with-shepherd-action 'jami ('list-contacts) results + (let ((contacts (assoc-ref (car results) #$username))) + (assert (lset= string-ci=? contacts '#$%allowed-contacts))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami accounts provisioning, moderators" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + ;; Moderators match those declared in the configuration. + (with-shepherd-action 'jami ('list-moderators) results + (let ((moderators (assoc-ref (car results) #$username))) + (assert (lset= string-ci=? moderators '#$%moderators)))) + + ;; Moderators can be added via the Shepherd action. + (with-shepherd-action 'jami + ('add-moderator "cccccccccccccccccccccccccccccccccccccccc" + #$username) results + (let ((moderators (car results))) + (assert (lset= string-ci=? moderators + (cons "cccccccccccccccccccccccccccccccccccccccc" + '#$%moderators)))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami service actions, ban/unban contacts" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + ;; Globally ban a contact. + (with-shepherd-action 'jami + ('ban-contact "1dbcb0f5f37324228235564b79f2b9737e9a008f") _ + (with-shepherd-action 'jami ('list-banned-contacts) results + (every (match-lambda + ((username . banned-contacts) + (member "1dbcb0f5f37324228235564b79f2b9737e9a008f" + banned-contacts))) + (car results)))) + + ;; Ban a contact for a single account. + (with-shepherd-action 'jami + ('ban-contact "dddddddddddddddddddddddddddddddddddddddd" + #$username) _ + (with-shepherd-action 'jami ('list-banned-contacts) results + (every (match-lambda + ((username . banned-contacts) + (let ((found? (member "dddddddddddddddddddddddddddddddddddddddd" + banned-contacts))) + (if (string=? #$username username) + found? + (not found?))))) + (car results))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami service actions, enable/disable accounts" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base)) + + (with-shepherd-action 'jami + ('disable-account #$username) _ + (with-shepherd-action 'jami ('list-accounts) results + (let ((account (assoc-ref (car results) #$username))) + (assert (string= "false" + (assoc-ref account "Account.enable")))))) + + (with-shepherd-action 'jami + ('enable-account #$username) _ + (with-shepherd-action 'jami ('list-accounts) results + (let ((account (assoc-ref (car results) #$username))) + (assert (string= "true" + (assoc-ref account "Account.enable"))))))) + marionette)) + + (unless #$provisioning? (test-skip 1)) + (test-assert "jami account parameters" + (marionette-eval + '(begin + (use-modules (gnu services herd) + (rnrs base) + (srfi srfi-1)) + + (with-shepherd-action 'jami ('list-account-details) results + (let ((account-details (assoc-ref (car results) + #$username))) + (assert (lset<= + equal? + '(("Account.hostname" . + "bootstrap.me;fallback.another.host") + ("Account.peerDiscovery" . "false") + ("Account.rendezVous" . "true") + ("RingNS.uri" . "https://my.name.server")) + account-details))))) + marionette)) + + (test-end) + (exit (= (test-runner-fail-count (test-runner-current)) 0))))) + + (gexp->derivation (if provisioning? + "jami-provisioning-test" + "jami-test") + test)) + +(define %test-jami + (system-test + (name "jami") + (description "Basic tests for the jami service.") + (value (run-jami-test)))) + +(define %test-jami-provisioning + (system-test + (name "jami-provisioning") + (description "Provisioning test for the jami service.") + (value (run-jami-test #:provisioning? #t)))) + +;; Local Variables: +;; eval: (put 'with-retries 'scheme-indent-function 2) +;; End: diff --git a/guix/self.scm b/guix/self.scm index 530632db7d..130f5d3492 100644 --- a/guix/self.scm +++ b/guix/self.scm @@ -974,6 +974,8 @@ (define *system-test-modules* (list *core-package-modules* *package-modules* *extra-modules* *system-modules* *core-modules* *cli-modules*) ;for (guix scripts pack), etc. + #:extra-files (file-imports source "gnu/tests/data" + (const #t)) #:extensions dependencies #:guile-for-build guile-for-build)) diff --git a/tests/services/telephony.scm b/tests/services/telephony.scm new file mode 100644 index 0000000000..b4a0f120d4 --- /dev/null +++ b/tests/services/telephony.scm @@ -0,0 +1,446 @@ +;;; GNU Guix --- Functional package management for GNU +;;; Copyright © 2021 Maxim Cournoyer +;;; +;;; This file is part of GNU Guix. +;;; +;;; GNU Guix is free software; you can redistribute it and/or modify it +;;; under the terms of the GNU General Public License as published by +;;; the Free Software Foundation; either version 3 of the License, or (at +;;; your option) any later version. +;;; +;;; GNU Guix 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 General Public License for more details. +;;; +;;; You should have received a copy of the GNU General Public License +;;; along with GNU Guix. If not, see . + +(define-module (tests services telephony) + #:use-module (gnu build jami-service) + #:use-module (gnu services telephony) + #:use-module (srfi srfi-64)) + +;;; Tests for the (gnu services telephony) and related modules. + +(test-begin "jami-service") + +(define parse-dbus-reply + (@@ (gnu build jami-service) parse-dbus-reply)) + +(define parse-account-ids + (@@ (gnu build jami-service) parse-account-ids)) + +(define parse-account-details + (@@ (gnu build jami-service) parse-account-details)) + +(define parse-contacts + (@@ (gnu build jami-service) parse-contacts)) + +(define jami-account->alist + (@@ (gnu services telephony) jami-account->alist)) + +;; $ dbus-send --print-reply --dest="cx.ring.Ring" \ +;; "/cx/ring/Ring/ConfigurationManager" \ +;; "cx.ring.Ring.ConfigurationManager.getAccountList" +(define getAccountList-reply "\ +method return time=1622217253.386711 sender=:1.7 -> destination=:1.14 serial=140 reply_serial=2 + array [ + string \"addf37fbb558d6a0\" + string \"d5cbeb7d08c98a65\" + string \"398af0c6b74ce101\" + ] +") + +(test-equal "parse-account-ids" + '("addf37fbb558d6a0" "d5cbeb7d08c98a65" "398af0c6b74ce101") + (parse-account-ids getAccountList-reply)) + +;; $ dbus-send --print-reply --dest="cx.ring.Ring" \ +;; "/cx/ring/Ring/ConfigurationManager" \ +;; "cx.ring.Ring.ConfigurationManager.getAccountDetails" \ +;; 'string:398af0c6b74ce101' +(define getAccountDetails-reply "\ +method return time=1622254991.789588 sender=:1.7 -> destination=:1.19 serial=145 reply_serial=2 + array [ + dict entry( + string \"Account.accountDiscovery\" + string \"false\" + ) + dict entry( + string \"Account.accountPublish\" + string \"false\" + ) + dict entry( + string \"Account.activeCallLimit\" + string \"-1\" + ) + dict entry( + string \"Account.alias\" + string \"some-rendezvous-point-name\" + ) + dict entry( + string \"Account.allModeratorEnabled\" + string \"true\" + ) + dict entry( + string \"Account.allowCertFromContact\" + string \"true\" + ) + dict entry( + string \"Account.allowCertFromHistory\" + string \"true\" + ) + dict entry( + string \"Account.allowCertFromTrusted\" + string \"true\" + ) + dict entry( + string \"Account.archiveHasPassword\" + string \"false\" + ) + dict entry( + string \"Account.audioPortMax\" + string \"32766\" + ) + dict entry( + string \"Account.audioPortMin\" + string \"16384\" + ) + dict entry( + string \"Account.autoAnswer\" + string \"false\" + ) + dict entry( + string \"Account.defaultModerators\" + string \"\" + ) + dict entry( + string \"Account.deviceID\" + string \"94b4070fc7a8afa8482c777a9822c52e6af2e1bd\" + ) + dict entry( + string \"Account.deviceName\" + string \"some-device\" + ) + dict entry( + string \"Account.dhtProxyListUrl\" + string \"https://config.jami.net/proxyList\" + ) + dict entry( + string \"Account.displayName\" + string \"some-rendezvous-point-name\" + ) + dict entry( + string \"Account.dtmfType\" + string \"overrtp\" + ) + dict entry( + string \"Account.enable\" + string \"true\" + ) + dict entry( + string \"Account.hasCustomUserAgent\" + string \"false\" + ) + dict entry( + string \"Account.hostname\" + string \"bootstrap.jami.net\" + ) + dict entry( + string \"Account.localInterface\" + string \"default\" + ) + dict entry( + string \"Account.localModeratorsEnabled\" + string \"true\" + ) + dict entry( + string \"Account.mailbox\" + string \"\" + ) + dict entry( + string \"Account.managerUri\" + string \"\" + ) + dict entry( + string \"Account.managerUsername\" + string \"\" + ) + dict entry( + string \"Account.peerDiscovery\" + string \"false\" + ) + dict entry( + string \"Account.presenceSubscribeSupported\" + string \"true\" + ) + dict entry( + string \"Account.proxyEnabled\" + string \"false\" + ) + dict entry( + string \"Account.proxyPushToken\" + string \"\" + ) + dict entry( + string \"Account.proxyServer\" + string \"dhtproxy.jami.net:[80-95]\" + ) + dict entry( + string \"Account.publishedAddress\" + string \"\" + ) + dict entry( + string \"Account.publishedPort\" + string \"5060\" + ) + dict entry( + string \"Account.publishedSameAsLocal\" + string \"true\" + ) + dict entry( + string \"Account.rendezVous\" + string \"true\" + ) + dict entry( + string \"Account.ringtoneEnabled\" + string \"true\" + ) + dict entry( + string \"Account.ringtonePath\" + string \"/usr/share/ring/ringtones/default.opus\" + ) + dict entry( + string \"Account.type\" + string \"RING\" + ) + dict entry( + string \"Account.upnpEnabled\" + string \"true\" + ) + dict entry( + string \"Account.useragent\" + string \"\" + ) + dict entry( + string \"Account.username\" + string \"ccb8bbe2382343f7feb140710ab48aaf1b55634e\" + ) + dict entry( + string \"Account.videoEnabled\" + string \"true\" + ) + dict entry( + string \"Account.videoPortMax\" + string \"65534\" + ) + dict entry( + string \"Account.videoPortMin\" + string \"49152\" + ) + dict entry( + string \"DHT.PublicInCalls\" + string \"true\" + ) + dict entry( + string \"DHT.port\" + string \"7766\" + ) + dict entry( + string \"RingNS.account\" + string \"3989b55313a911b6f0c004748b49b254f35c9ef6\" + ) + dict entry( + string \"RingNS.uri\" + string \"\" + ) + dict entry( + string \"SRTP.enable\" + string \"true\" + ) + dict entry( + string \"SRTP.keyExchange\" + string \"sdes\" + ) + dict entry( + string \"SRTP.rtpFallback\" + string \"false\" + ) + dict entry( + string \"STUN.enable\" + string \"false\" + ) + dict entry( + string \"STUN.server\" + string \"\" + ) + dict entry( + string \"TLS.certificateFile\" + string \"/var/lib/jami/.local/share/jami/398af0c6b74ce101/ring_device.crt\" + ) + dict entry( + string \"TLS.certificateListFile\" + string \"\" + ) + dict entry( + string \"TLS.ciphers\" + string \"\" + ) + dict entry( + string \"TLS.method\" + string \"Automatic\" + ) + dict entry( + string \"TLS.negotiationTimeoutSec\" + string \"-1\" + ) + dict entry( + string \"TLS.password\" + string \"\" + ) + dict entry( + string \"TLS.privateKeyFile\" + string \"/var/lib/jami/.local/share/jami/398af0c6b74ce101/ring_device.key\" + ) + dict entry( + string \"TLS.requireClientCertificate\" + string \"true\" + ) + dict entry( + string \"TLS.serverName\" + string \"\" + ) + dict entry( + string \"TLS.verifyClient\" + string \"true\" + ) + dict entry( + string \"TLS.verifyServer\" + string \"true\" + ) + dict entry( + string \"TURN.enable\" + string \"true\" + ) + dict entry( + string \"TURN.password\" + string \"ring\" + ) + dict entry( + string \"TURN.realm\" + string \"ring\" + ) + dict entry( + string \"TURN.server\" + string \"turn.jami.net\" + ) + dict entry( + string \"TURN.username\" + string \"ring\" + ) + ] +") + +(test-equal "parse-account-details; username, alias and display name" + '("ccb8bbe2382343f7feb140710ab48aaf1b55634e" ;username + "some-rendezvous-point-name" ;alias + "some-rendezvous-point-name") ;displayName + (let ((account-details (parse-account-details getAccountDetails-reply))) + (list (assoc-ref account-details "Account.username") + (assoc-ref account-details "Account.alias") + (assoc-ref account-details "Account.displayName")))) + +(define getContacts-reply "\ +method return time=1627014042.752673 sender=:1.113 -> destination=:1.186 serial=220 reply_serial=2 + array [ + array [ + dict entry( + string \"added\" + string \"1578883327\" + ) + dict entry( + string \"confirmed\" + string \"true\" + ) + dict entry( + string \"id\" + string \"1c7d5a09464223442549fef172a3cf6f4de9b01c\" + ) + ] + array [ + dict entry( + string \"added\" + string \"1623107941\" + ) + dict entry( + string \"confirmed\" + string \"true\" + ) + dict entry( + string \"id\" + string \"5903c6c9ac5cb863c64e559add3d5d1c8c563449\" + ) + ] + array [ + dict entry( + string \"added\" + string \"1595996256\" + ) + dict entry( + string \"confirmed\" + string \"true\" + ) + dict entry( + string \"id\" + string \"ff2d72a548693214fb3a0f0f7a943b5e2bb9be03\" + ) + ] + ]") + +(test-equal "parse-account-contacts" + '((("added" . "1578883327") + ("confirmed" . "true") + ("id" . "1c7d5a09464223442549fef172a3cf6f4de9b01c")) + (("added" . "1623107941") + ("confirmed" . "true") + ("id" . "5903c6c9ac5cb863c64e559add3d5d1c8c563449")) + (("added" . "1595996256") + ("confirmed" . "true") + ("id" . "ff2d72a548693214fb3a0f0f7a943b5e2bb9be03"))) + (parse-contacts getContacts-reply)) + +(define getContacts-empty-reply "\ +method return time=1627400787.873988 sender=:1.1197 -> destination=:1.1463 serial=2127 reply_serial=2 + array [ + ]") + +(test-equal "parse-account-contacts, empty array" + '() + (parse-contacts getContacts-empty-reply)) + +(define %dummy-jami-account (jami-account + (archive "/tmp/dummy.gz"))) + +(define %dummy-jami-account-2 (jami-account + (archive "/tmp/dummy.gz") + (rendezvous-point? #t) + (peer-discovery? #f) + (bootstrap-hostnames '("bootstrap.me" + "fallback.another.host")) + (name-server-uri "https://my.name.server"))) + +(test-equal "jami-account->alist, no account detail value set" + '() + (jami-account->alist %dummy-jami-account)) + +(test-equal "jami-account->alist, with account detail values" + '(("Account.hostname" . "bootstrap.me;fallback.another.host") + ("Account.peerDiscovery" . "false") + ("Account.rendezVous" . "true") + ("RingNS.uri" . "https://my.name.server")) + (sort (jami-account->alist %dummy-jami-account-2) + (lambda (x y) + (string<=? (car x) (car y))))) + +(test-end) -- cgit v1.2.3