GNU bug report logs - #47849
[PATCH] Add a jami-daemon service.

Previous Next

Package: guix-patches;

Reported by: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>

Date: Sat, 17 Apr 2021 20:05:01 UTC

Severity: normal

Tags: patch

Done: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>

Bug is archived. No further changes may be made.

To add a comment to this bug, you must first unarchive it, by sending
a message to control AT debbugs.gnu.org, with unarchive 47849 in the body.
You can then email your comments to 47849 AT debbugs.gnu.org in the normal way.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sat, 17 Apr 2021 20:05:01 GMT) Full text and rfc822 format available.

Acknowledgement sent to Maxim Cournoyer <maxim.cournoyer <at> gmail.com>:
New bug report received and forwarded. Copy sent to guix-patches <at> gnu.org. (Sat, 17 Apr 2021 20:05:01 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: guix-patches <at> gnu.org
Cc: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: [PATCH 0/1] [preview] Add a jami-daemon service.
Date: Sat, 17 Apr 2021 16:04:14 -0400
Hello,

This is an early version of a jami-daemon service that can be used to host
rendezvous points easily on servers, the conferencing feature of Jami.

It seems to work well in practice, but I've been struggling to fix a remaining
test failure with the stop action.  It's proving difficult to pinpoint what
the problem is.

Another thing that will need to be addressed is adding the documentation.

Thanks,

Maxim

Maxim Cournoyer (1):
  services: Add a service for the Jami daemon.

 gnu/local.mk                          |   6 +-
 gnu/services/telephony.scm            | 283 ++++++++++++++++++-
 gnu/tests/data/jami-dummy-account.dat | 391 ++++++++++++++++++++++++++
 gnu/tests/telephony.scm               | 202 +++++++++++++
 4 files changed, 878 insertions(+), 4 deletions(-)
 create mode 100644 gnu/tests/data/jami-dummy-account.dat
 create mode 100644 gnu/tests/telephony.scmb

-- 
2.31.1





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sat, 17 Apr 2021 20:07:02 GMT) Full text and rfc822 format available.

Message #8 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: 47849 <at> debbugs.gnu.org
Cc: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: [PATCH 1/1] services: Add a service for the Jami daemon.
Date: Sat, 17 Apr 2021 16:06:17 -0400
* gnu/services/telephony.scm (serialize-boolean)
(serialize-string-list, string-list?): New variables.
(maybe-string-list, jami-daemon-configuration): New syntax.
(%jami-daemon-accounts): New variable.
(jami-daemon-configuration->command-line-arguments): New procedure.
(jami-dbus-session-activation): Likewise.
(define-with-retries, define-send-dbus)
(jami-daemon-shepherd-services, jami-daemon-service-type): New variables.
* gnu/tests/data/jami-dummy-account.dat: New file.
* gnu/tests/telephony.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Register the new test file.
(dist_patch_DATA): Register the new data file.
---
 gnu/local.mk                          |   6 +-
 gnu/services/telephony.scm            | 283 ++++++++++++++++++-
 gnu/tests/data/jami-dummy-account.dat | 391 ++++++++++++++++++++++++++
 gnu/tests/telephony.scm               | 202 +++++++++++++
 4 files changed, 878 insertions(+), 4 deletions(-)
 create mode 100644 gnu/tests/data/jami-dummy-account.dat
 create mode 100644 gnu/tests/telephony.scm

diff --git a/gnu/local.mk b/gnu/local.mk
index 50b11a8ca2..4a412f5a69 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -19,7 +19,7 @@
 # Copyright © 2018 Amirouche Boubekki <amirouche <at> hypermove.net>
 # Copyright © 2018, 2019, 2020, 2021 Oleg Pykhalov <go.wigust <at> gmail.com>
 # Copyright © 2018 Stefan Stefanović <stefanx2ovic <at> gmail.com>
-# Copyright © 2018, 2020 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
+# Copyright © 2018, 2020, 2021 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
 # Copyright © 2019, 2020 Guillaume Le Vaillant <glv <at> posteo.net>
 # Copyright © 2019, 2020 John Soo <jsoo1 <at> asu.edu>
 # Copyright © 2019 Jonathan Brielmaier <jonathan.brielmaier <at> web.de>
@@ -710,6 +710,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
@@ -1829,7 +1830,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..9e821f7286 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  <nee-git <at> hidamari.blue>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -17,16 +18,31 @@
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (gnu services telephony)
-  #:use-module (gnu services)
+  #: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 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 (ice-9 match)
-  #:export (murmur-configuration
+  #:export (jami-daemon-configuration
+            jami-daemon-configuration-jami-daemon
+            jami-daemon-configuration-dbus
+            jami-daemon-configuration-enable-logging?
+            jami-daemon-configuration-debug?
+            jami-daemon-configuration-auto-answer?
+            jami-daemon-configuration-account-archives
+
+            jami-daemon-service-type
+
+            murmur-configuration
             make-murmur-configuration
             murmur-configuration?
             murmur-configuration-package
@@ -74,6 +90,269 @@
 
             murmur-service-type))
 
+
+
+;;;
+;;; Jami daemon.
+;;;
+
+;;; XXX: These dummy definitions is because there's no way to disable the
+;;; serialization code from define-configuration.
+(define (serialize-boolean option value) "")
+(define (serialize-string-list field-name val) "")
+
+;;; Copied from (gnu services messaging).
+(define (string-list? val)
+  (and (list? val)
+       (and-map (lambda (x)
+                  (or (computed-file? x) ;XXX: for tests
+                      (and (string? x) (not (string-index x #\,)))))
+                val)))
+(define-maybe string-list)
+
+(define-configuration jami-daemon-configuration
+  (jami-daemon
+   (package libring)
+   "The Jami daemon package to use.")
+  (dbus
+   (package dbus)
+   "The D-Bus package to use to start the required D-Bus session.")
+  (enable-logging?
+   (boolean #true)
+   "Whether to enable logging to syslog.")
+  (debug?
+   (boolean #false)
+   "Whether to enable debug level messages.")
+  (auto-answer?
+   (boolean #false)
+   "Whether to force automatic answer to incoming calls.")
+  (account-archives
+   (maybe-string-list 'disabled)
+   "A list of Jami account archive (backup) file names to be (re-)provisioned
+every time the Jami daemon service starts.  These Jami account backups should
+@emp{not} be encrypted and their file should be made readable only to the
+@samp{jami} user (i.e., not in the store), to guard against leaking the secret
+key material of the Jami accounts they contain.  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-daemon-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-daemon-configuration->command-line-arguments config)
+  "Derive the command line arguments to used to launch the Jami daemon from
+CONFIG, a <jami-daemon-configuration> object."
+  (match-record config <jami-daemon-configuration>
+    (jami-daemon dbus enable-logging? debug? auto-answer?)
+    `(,(file-append jami-daemon "/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)))))
+
+;; Local definitions to expand in source form in G-exps.
+(define define-with-retries
+  '(define-syntax-rule (with-retries n delay body ...)
+     "Retry the code in BODY up to N times until it returns #t,
+else #f.  A delay of DELAY seconds is inserted before each retry."
+     (let loop ((attempts 0))
+       (if (< attempts n)
+           (or (begin
+                 body ...)              ;return #t on success
+               (begin
+                 (sleep delay)          ;else wait and retry
+                 (loop (+ 1 attempts))))
+           #f))))                   ;maximum number of attempts reached
+
+(define define-send-dbus
+  '(define (send-dbus dbus-send interface method . arguments)
+     "Print the response and return #t on success, else #f."
+     (let* ((command `(,dbus-send
+                       "--bus=unix:path=/var/run/jami/bus"
+                       "--print-reply"
+                       "--dest=cx.ring.Ring"
+                       "/cx/ring/Ring/ConfigurationManager" ;object path
+                       ,(string-append interface "." method)
+                       ,@arguments))
+            (pid (fork+exec-command command
+                                    #:user "jami"
+                                    #:group "jami")))
+       (zero? (cdr (waitpid pid))))))
+
+(define (jami-daemon-shepherd-services config)
+  "Return a <shepherd-service> running the Jami daemon."
+  (let* ((jami-daemon (jami-daemon-configuration-jami-daemon config))
+         (dbus (jami-daemon-configuration-dbus config))
+         (dbus-daemon (file-append dbus "/bin/dbus-daemon"))
+         (dbus-send (file-append dbus "/bin/dbus-send"))
+         (accounts (jami-daemon-configuration-account-archives config))
+         (declarative-mode? (not (eq? 'disabled accounts))))
+
+    (list (shepherd-service
+           (documentation "Run a D-Bus session for the Jami daemon.")
+           (provision '(jami-daemon-dbus-session))
+           ;; 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-with-retries
+
+                (define pid (fork+exec-command
+                             (list #$dbus-daemon "--session"
+                                   "--address=unix:path=/var/run/jami/bus"
+                                   "--nofork" "--syslog-only" "--nopidfile")
+                             #: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="
+                                                  #$jami-daemon "/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 the <pidfile> 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-daemon dring))
+           (requirement '(jami-daemon-dbus-session))
+           (modules `((ice-9 ftw)
+                      (srfi srfi-1)
+                      (srfi srfi-26)
+                      ,@%default-modules))
+           (start
+            #~(lambda args
+                #$define-with-retries
+                #$define-send-dbus
+
+                (when #$declarative-mode?
+                  ;; Clear the Jami configuration and accounts, to enforce the
+                  ;; declared state.
+                  (catch #t
+                    (lambda ()
+                      (delete-file-recursively "/var/lib/jami/.cache/jami")
+                      (delete-file-recursively "/var/lib/jami/.config/jami")
+                      (delete-file-recursively "/var/lib/jami/.local/share/jami")
+                      (delete-file-recursively "/var/lib/jami/accounts"))
+                    (lambda args
+                      #t))
+                  ;; Copy the Jami accounts 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)))
+                              '#$accounts)))
+
+                ;; Start the daemon.
+                (define daemon-pid
+                  (fork+exec-command
+                   '#$(jami-daemon-configuration->command-line-arguments config)
+                   #:user "jami"
+                   #:group "jami"
+                   #:environment-variables
+                   (list (string-append "DBUS_SESSION_BUS_ADDRESS="
+                                        "unix:path=/var/run/jami/bus"))))
+
+                ;; Wait until it's reachable via D-Bus.
+                (with-retries 20 1 (send-dbus #$dbus-send
+                                              "org.freedesktop.DBus.Peer"
+                                              "Ping"))
+
+                ;; Provision the accounts.
+                (when #$declarative-mode?
+                  (or (every identity
+                             (map (lambda (archive)
+                                    (send-dbus
+                                     #$dbus-send
+                                     "cx.ring.Ring.ConfigurationManager"
+                                     "addAccount"
+                                     (string-append
+                                      "dict:string:string:Account.archivePath,"
+                                      archive
+                                      ",Account.type,RING")))
+                                  (map (cut string-append
+                                            "/var/lib/jami/accounts/" <>)
+                                       (scandir "/var/lib/jami/accounts/"
+                                                (lambda (f)
+                                                  (not (member f '("." ".."))))))))
+                      (format (current-error-port)
+                              "error: failed provisioning the jami accounts")))
+
+                ;; Finally, return the PID of the dring process.
+                daemon-pid))
+           (stop
+            #~(lambda (pid . args)
+                (kill pid SIGTERM)
+                ;; Wait for the process to exit; this prevents overlapping
+                ;; processes when issuing 'herd restart'.
+                (waitpid pid)
+                #f))))))
+
+(define jami-daemon-service-type
+  (service-type
+   (name 'jami-daemon)
+   (default-value (jami-daemon-configuration))
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             jami-daemon-shepherd-services)
+          (service-extension account-service-type
+                             (const %jami-daemon-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> murmur-configuration
diff --git a/gnu/tests/data/jami-dummy-account.dat b/gnu/tests/data/jami-dummy-account.dat
new file mode 100644
index 0000000000..026890052d
--- /dev/null
+++ b/gnu/tests/data/jami-dummy-account.dat
@@ -0,0 +1,391 @@
+;;; 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..cdcd00d057
--- /dev/null
+++ b/gnu/tests/telephony.scm
@@ -0,0 +1,202 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer <at> gnu.org>.
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(define-module (gnu tests telephony)
+  #:use-module (gnu)
+  #: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 ssh)
+  #:use-module (gnu services telephony)
+  #:use-module (guix gexp)
+  #:use-module (guix modules)
+  #:export (%test-jami-daemon
+            %test-jami-daemon-provisioning))
+
+;;;
+;;; Jami daemon.
+;;;
+
+(define define-with-retries (@@ (gnu services telephony) define-with-retries))
+
+(include "data/jami-dummy-account.dat") ;defines %jami-account-content-sexp
+
+(define %dummy-jami-account
+  ;; 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* (make-jami-daemon-os #:key provisioning?)
+  (simple-operating-system
+   (service jami-daemon-service-type
+            (if provisioning?
+                (jami-daemon-configuration
+                 (account-archives (list %dummy-jami-account)))
+                (jami-daemon-configuration)))
+   (service dbus-root-service-type)
+   (service openssh-service-type        ;for debugging
+            (openssh-configuration
+             (permit-root-login 'without-password)))))
+
+(define %jami-daemon-os
+  (make-jami-daemon-os))
+
+(define %jami-daemon-os-provisioning
+  (make-jami-daemon-os #:provisioning? #t))
+
+(define* (run-jami-daemon-test #:key provisioning?)
+  "Run tests in %JAMI-DAEMON-OS.  When PROVISIONING? is true, test the
+accounts provisioning feature of the service."
+  (define os (marionette-operating-system
+              (if provisioning?
+                  %jami-daemon-os-provisioning
+                  %jami-daemon-os)
+              #:imported-modules '((gnu services herd)
+                                   (guix combinators))))
+  (define vm (virtual-machine
+              (operating-system os)
+              (memory-size 512)))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (rnrs base)
+                       (srfi srfi-11)
+                       (srfi srfi-64)
+                       (gnu build marionette))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          (mkdir #$output)
+          (chdir #$output)
+
+          (test-begin "jami-daemon")
+
+          (test-assert "service running"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (match (start-service 'jami-daemon)
+                  (#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))
+                #$define-with-retries
+                (setenv "PATH" "/run/current-system/profile/bin")
+                (let ((pid (match (start-service 'jami-daemon)
+                             (#f #f)
+                             (('service response-parts ...)
+                              (match (assq-ref response-parts 'running)
+                                ((pid) pid))))))
+
+                  (assert (number? pid))
+
+                  (match (stop-service 'jami-daemon)
+                    (services           ;a list of service symbols
+                     (member 'jami-daemon 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
+                   19 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-daemon)
+                              (#f #f)
+                              (('service response-parts ...)
+                               (match (assq-ref response-parts 'running)
+                                 ((pid) pid)))))
+                (assert (number? pid))
+
+                ;; Restart the service.
+                (restart-service 'jami-daemon)
+
+                (define new-pid (match (start-service 'jami-daemon)
+                                  (#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"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+
+                #$define-with-retries
+
+                (setenv "PATH" "/run/current-system/profile/bin")
+                ;; The imported account takes some time to appear in the
+                ;; account files; retry for up to about 10 s.
+                (with-retries
+                 20 1
+                 (zero? (status:exit-val
+                         (system* "grep" "-r"
+                                  #$(assoc-ref %jami-account-content-sexp
+                                               "Account.username")
+                                  "/var/lib/jami/.local/share/jami/")))))
+             marionette))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+
+  (gexp->derivation (if provisioning?
+                        "jami-daemon-provisioning-test"
+                        "jami-daemon-test")
+                    test))
+
+(define %test-jami-daemon
+  (system-test
+   (name "jami-daemon")
+   (description "Basic tests for the jami-daemon service.")
+   (value (run-jami-daemon-test))))
+
+(define %test-jami-daemon-provisioning
+  (system-test
+   (name "jami-daemon-provisioning")
+   (description "Provisioning test for the jami-daemon service.")
+   (value (run-jami-daemon-test #:provisioning? #t))))
-- 
2.31.1





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 18 Apr 2021 12:14:02 GMT) Full text and rfc822 format available.

Message #11 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxime Devos <maximedevos <at> telenet.be>
To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>, 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Sun, 18 Apr 2021 13:41:06 +0200
[Message part 1 (text/plain, inline)]
Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
+                      (delete-file-recursively "/var/lib/jami/.cache/jami")
+                      (delete-file-recursively "/var/lib/jami/.config/jami")
+                      (delete-file-recursively "/var/lib/jami/.local/share/jami")
+                      (delete-file-recursively "/var/lib/jami/accounts"))

You might want to verify whether /var/lib/jami/{.cache,.config,.local/share,.local}
aren't symbolic links.  That way, if the Jami daemon is compromised (due to buffer
overflow --> arbitrary code execution or something), the attacker can't trick the
shepherd service into deleting arbitrary directories.

This attack is _not_ blocked by fs.protected_symlinks.  From the sysctl documentation:
  When set to "1" symlinks are permitted to be followed only when outside
  a sticky world-writable directory, or [...]

/var/lib/jami is not world-writable (I'd hope).

Example scenario:
  * the jami daemon has a security bug that allows arbitrary code execution
    within the daemon
  * the attacker exploits this
  * now the attacker can modify everything under /var/lib/jami
  * the attacker deletes /var/lib/jami/.config and replaces it with a symlink
    to /home/ANY-USER/.config
  * eventually, the system reboots
  * (delete-file-recursively "/var/lib/jami/.config/jami") is run.
    As "/var/lib/jami/.config" points to "/home/ANY-USER/.config",
    this means "/home/ANY-USER/.config/jami" is deleted.
  * thus, ANY-USER loses their jami configuration

Does that makes sense to you?

Greetings,
Maxime.
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 18 Apr 2021 12:14:03 GMT) Full text and rfc822 format available.

Message #14 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxime Devos <maximedevos <at> telenet.be>
To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>, 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Sun, 18 Apr 2021 13:47:03 +0200
[Message part 1 (text/plain, inline)]
Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
> +                ;; Start the daemon.
> +                (define daemon-pid
> +                  (fork+exec-command
> +                   '#$(jami-daemon-configuration->command-line-arguments config)
> +                   #:user "jami"
> +                   #:group "jami"
> +                   #:environment-variables
> +                   (list (string-append "DBUS_SESSION_BUS_ADDRESS="
> +                                        "unix:path=/var/run/jami/bus"))))

It would be nice if this could be run in a container
that only has access to the relevant parts of the file system
(and not, say, /run/setuid-programs).  See, e.g., gnu/build/linux-container.scm.

Greetings,
Maxime.
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Mon, 19 Apr 2021 12:08:02 GMT) Full text and rfc822 format available.

Message #17 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Maxime Devos <maximedevos <at> telenet.be>
Cc: 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Mon, 19 Apr 2021 08:07:25 -0400
Hi Maxime!

Maxime Devos <maximedevos <at> telenet.be> writes:

> Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
> +                      (delete-file-recursively "/var/lib/jami/.cache/jami")
> +                      (delete-file-recursively "/var/lib/jami/.config/jami")
> +                      (delete-file-recursively "/var/lib/jami/.local/share/jami")
> +                      (delete-file-recursively "/var/lib/jami/accounts"))
>
> You might want to verify whether /var/lib/jami/{.cache,.config,.local/share,.local}
> aren't symbolic links.  That way, if the Jami daemon is compromised (due to buffer
> overflow --> arbitrary code execution or something), the attacker can't trick the
> shepherd service into deleting arbitrary directories.

It would only be able to delete directories that are world writable
though, right?  Seems the opportunity to cause damage is limited, but
it's a simple check to add, so I'll do it.  What about if the daemon was
run in a container (your suggestion in a following email, to which I
agree would be a good thing)?  It would prevent this kind of attack,
right?

> This attack is _not_ blocked by fs.protected_symlinks.  From the sysctl documentation:
>   When set to "1" symlinks are permitted to be followed only when outside
>   a sticky world-writable directory, or [...]
>
> /var/lib/jami is not world-writable (I'd hope).

No, it's only readable/writable by the 'jami' user of the service:

$ sudo  ls -ald /var/lib/jami
drwx------ 1 jami jami 80 Apr 19 00:38 /var/lib/jami

> Example scenario:
>   * the jami daemon has a security bug that allows arbitrary code execution
>     within the daemon
>   * the attacker exploits this
>   * now the attacker can modify everything under /var/lib/jami
>   * the attacker deletes /var/lib/jami/.config and replaces it with a symlink
>     to /home/ANY-USER/.config
>   * eventually, the system reboots
>   * (delete-file-recursively "/var/lib/jami/.config/jami") is run.
>     As "/var/lib/jami/.config" points to "/home/ANY-USER/.config",
>     this means "/home/ANY-USER/.config/jami" is deleted.
>   * thus, ANY-USER loses their jami configuration

The cleanup code is run as the 'jami' user, so I don't think it'd be
able to touch anything under /home/ANY-OTHER-USER/, unless they manually
loosened permissions on their home directory (shooting themselves in the
foot).

> Does that makes sense to you?

It does!  Thanks for explaining.

Maxim




Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Mon, 19 Apr 2021 12:09:01 GMT) Full text and rfc822 format available.

Message #20 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Maxime Devos <maximedevos <at> telenet.be>
Cc: 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Mon, 19 Apr 2021 08:08:34 -0400
Hi again,

Maxime Devos <maximedevos <at> telenet.be> writes:

> Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
>> +                ;; Start the daemon.
>> +                (define daemon-pid
>> +                  (fork+exec-command
>> +                   '#$(jami-daemon-configuration->command-line-arguments config)
>> +                   #:user "jami"
>> +                   #:group "jami"
>> +                   #:environment-variables
>> +                   (list (string-append "DBUS_SESSION_BUS_ADDRESS="
>> +                                        "unix:path=/var/run/jami/bus"))))
>
> It would be nice if this could be run in a container
> that only has access to the relevant parts of the file system
> (and not, say, /run/setuid-programs).  See, e.g., gnu/build/linux-container.scm.

I agree!  The opendht service is already doing so, so it should not be
too difficult I believe.  I'll try looking into it, after the 1.3.0
release is done.

Thank you!

Maxim




Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Mon, 19 Apr 2021 14:42:03 GMT) Full text and rfc822 format available.

Message #23 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxime Devos <maximedevos <at> telenet.be>
To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Cc: 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Mon, 19 Apr 2021 16:41:20 +0200
[Message part 1 (text/plain, inline)]
Maxim Cournoyer schreef op ma 19-04-2021 om 08:07 [-0400]:
> Hi Maxime!
> 
> Maxime Devos <maximedevos <at> telenet.be> writes:
> 
> > Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
> > +                      [...]
> > +                       (delete-file-recursively "/var/lib/jami/accounts"))
> > 
> > You might want to verify whether /var/lib/jami/{.cache,.config,.local/share,.local}
> > aren't symbolic links.  That way, if the Jami daemon is compromised (due to buffer
> > overflow --> arbitrary code execution or something), the attacker can't trick the
> > shepherd service into deleting arbitrary directories.
> 
> It would only be able to delete directories that are world writable
> though, right?  Seems the opportunity to cause damage is limited, but
> it's a simple check to add, so I'll do it.

Let's step through the relevant code of the shepherd service.

(shepherd-service
  (documentation "Run the Jami daemon.")
  [blabla]
  (start
    #~(lambda args
        [other stuff]
        (when [blabla, and a 'catch' form]
          (delete-file-recursively "/var/lib/jami/.cache/jami")
          [etcetera])
         (let* (([blabla])
                (user (passwd:uid [blabla jami user]))
                (group [likewise]))
           [blabla] (chown accounts-dir user group)))
        ;; Start the daemon
        (define daemon-pid
          (fork+exec-command [blabla Jami cmdline arguments]
            #:user "jami" #:group "jami" [blabla]))
        [blabla]))
   (stop [blabla]))         

Remember that the shepherd daemon is run as root (and therefore
has read-write-execute access to everything).  The 'start' procedure
(and 'stop' procedure) are run _within_ the shepherd daemon.  Thus, the
'start' gexp is run as root.

As the start procedure didn't change the uid/gid from root to something
else, (delete-file-recursively "/var/lib/jami/.cache/jami") is run as
root.  IIUC, root user can read/write anything, ignoring things like "user"
and "group".  World-writability is not required.

> What about if the daemon was
> run in a container (your suggestion in a following email, to which I
> agree would be a good thing)?  It would prevent this kind of attack,
> right?

I don't see how that would help.  It is the _shepherd daemon_ (that runs
as root) that runs (delete-file-recursively ...), not the attacker (from
within the compromised jami-daemon process).  Perhaps this is cleared up
by my previous response?  If not, please walk me through the attack scenario
you had in mind that would be thwarted by running the jami-daemon in a container.

> > Example scenario:
> >   * the jami daemon has a security bug that allows arbitrary code execution
> >     within the daemon
> >   * the attacker exploits this
> >   * now the attacker can modify everything under /var/lib/jami
> >   * the attacker deletes /var/lib/jami/.config and replaces it with a symlink
> >     to /home/ANY-USER/.config
> >   * eventually, the system reboots
> >   * (delete-file-recursively "/var/lib/jami/.config/jami") is run.
> >     As "/var/lib/jami/.config" points to "/home/ANY-USER/.config",
> >     this means "/home/ANY-USER/.config/jami" is deleted.
> >   * thus, ANY-USER loses their jami configuration
> 
> The cleanup code is run as the 'jami' user,

Err, the cleanup code is run from within the shepherd service, outside the
fork+exec-command.  The shepherd service is run _within_ the shepherd daemon,
which runs as *root*.  Only in the fork+exec-command, code is run as the 'jami'
user; the cleanup code is run as 'root'.

> so I don't think it'd be able to touch anything under /home/ANY-OTHER-USER/,
> unless they manually loosened permissions on their home directory (shooting
> themselves in the foot).
See my comment above.

Greetings,
Maxime.
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Mon, 19 Apr 2021 15:43:02 GMT) Full text and rfc822 format available.

Message #26 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Maxime Devos <maximedevos <at> telenet.be>
Cc: 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Mon, 19 Apr 2021 11:42:13 -0400
Hi Maxime,

Maxime Devos <maximedevos <at> telenet.be> writes:

> Maxim Cournoyer schreef op ma 19-04-2021 om 08:07 [-0400]:
>> Hi Maxime!
>> 
>> Maxime Devos <maximedevos <at> telenet.be> writes:
>> 
>> > Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
>> > +                      [...]
>> > +                       (delete-file-recursively "/var/lib/jami/accounts"))
>> > 
>> > You might want to verify whether /var/lib/jami/{.cache,.config,.local/share,.local}
>> > aren't symbolic links.  That way, if the Jami daemon is compromised (due to buffer
>> > overflow --> arbitrary code execution or something), the attacker can't trick the
>> > shepherd service into deleting arbitrary directories.
>> 
>> It would only be able to delete directories that are world writable
>> though, right?  Seems the opportunity to cause damage is limited, but
>> it's a simple check to add, so I'll do it.
>
> Let's step through the relevant code of the shepherd service.
>
> (shepherd-service
>   (documentation "Run the Jami daemon.")
>   [blabla]
>   (start
>     #~(lambda args
>         [other stuff]
>         (when [blabla, and a 'catch' form]
>           (delete-file-recursively "/var/lib/jami/.cache/jami")
>           [etcetera])
>          (let* (([blabla])
>                 (user (passwd:uid [blabla jami user]))
>                 (group [likewise]))
>            [blabla] (chown accounts-dir user group)))
>         ;; Start the daemon
>         (define daemon-pid
>           (fork+exec-command [blabla Jami cmdline arguments]
>             #:user "jami" #:group "jami" [blabla]))
>         [blabla]))
>    (stop [blabla]))         
>
> Remember that the shepherd daemon is run as root (and therefore
> has read-write-execute access to everything).  The 'start' procedure
> (and 'stop' procedure) are run _within_ the shepherd daemon.  Thus, the
> 'start' gexp is run as root.

Ah yes, looking the service definition it's obvious.  Sorry for missing
that earlier :-).

> As the start procedure didn't change the uid/gid from root to something
> else, (delete-file-recursively "/var/lib/jami/.cache/jami") is run as
> root.  IIUC, root user can read/write anything, ignoring things like "user"
> and "group".  World-writability is not required.
>
>> What about if the daemon was
>> run in a container (your suggestion in a following email, to which I
>> agree would be a good thing)?  It would prevent this kind of attack,
>> right?
>
> I don't see how that would help.  It is the _shepherd daemon_ (that runs
> as root) that runs (delete-file-recursively ...), not the attacker (from
> within the compromised jami-daemon process).  Perhaps this is cleared up
> by my previous response?

Indeed!  Thanks for taking the time to make it clear for me!

I'll address this in a v2 patch series, hopefully resolving the issues
with the daemon starting in double the first time the system is
reconfigured (something to do with d-bus autospawning I think -- perhaps
the D-Bus ping method is enough to spawn the process if it was not yet
up and running).

It'll take me some time to get to it; probably after the v1.3.0 is
released.

Thank you!

Maxim




Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Thu, 20 May 2021 12:33:01 GMT) Full text and rfc822 format available.

Message #29 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: 47849 <at> debbugs.gnu.org
Cc: Maxime Devos <maximedevos <at> telenet.be>,
 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: [PATCH v2] services: Add a service for the Jami daemon.
Date: Thu, 20 May 2021 08:31:10 -0400
* gnu/services/telephony.scm (string-list?): New procedure.
(maybe-string-list?): Likewise.
(jami-daemon-configuration): New configuration.
(%jami-daemon-accounts): New variable.
(jami-daemon-configuration->command-line-arguments): New procedure.
(jami-dbus-session-activation): Likewise.
(define-with-retries, define-send-dbus)
(jami-daemon-service-type): New variables.
(jami-daemon-shepherd-services): New procedure.
* gnu/tests/data/jami-dummy-account.dat: New file.
* gnu/tests/telephony.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Register the new test file.
(dist_patch_DATA): Register the new data file.
* doc/guix.texi (Telephony Services): Document it.
---
 doc/guix.texi                         |  77 +++++
 gnu/local.mk                          |   4 +-
 gnu/services/telephony.scm            | 355 ++++++++++++++++++++++-
 gnu/tests/data/jami-dummy-account.dat | 391 ++++++++++++++++++++++++++
 gnu/tests/telephony.scm               | 204 ++++++++++++++
 5 files changed, 1028 insertions(+), 3 deletions(-)
 create mode 100644 gnu/tests/data/jami-dummy-account.dat
 create mode 100644 gnu/tests/telephony.scm

diff --git a/doc/guix.texi b/doc/guix.texi
index e8b0485f78..a58fe0a44e 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -22435,6 +22435,83 @@ 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 (daemon)
+
+@cindex jami-daemon, service
+
+This section describes how to configure a Jami server that can be used
+to host video (or audio) conferences.  Here's an example that
+demonstrates how to specify account archives (Jami backups) to be
+provisioned automatically:
+
+@lisp
+(service jami-daemon-service-type
+         (jami-daemon-configuration
+          (account-archives
+           '("/etc/jami/unencrypted-account-1.gz"
+             "/etc/jami/unencrypted-account-2.gz"))))
+@end lisp
+
+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 complete set of
+available configuration options are detailed below.
+
+Available @code{jami-daemon-configuration} fields are:
+
+@deftypevr {@code{jami-daemon-configuration} parameter} package jami-daemon
+The Jami daemon package to use.
+
+@end deftypevr
+
+@deftypevr {@code{jami-daemon-configuration} parameter} package dbus
+The D-Bus package to use to start the required D-Bus session.
+
+@end deftypevr
+
+@deftypevr {@code{jami-daemon-configuration} parameter} boolean enable-logging?
+Whether to enable logging to syslog.
+
+Defaults to @samp{#t}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-daemon-configuration} parameter} boolean debug?
+Whether to enable debug level messages.
+
+Defaults to @samp{#f}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-daemon-configuration} parameter} boolean auto-answer?
+Whether to force automatic answer to incoming calls.
+
+Defaults to @samp{#f}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-daemon-configuration} parameter} maybe-string-list account-archives
+A list of Jami account archive (backup) file names to be
+(re-)provisioned every time the Jami daemon service starts.  These Jami
+account backups should @emph{not} be encrypted and should be made
+readable only to the @samp{root} user (i.e., not in the store), to guard
+against leaking the secret key material of the Jami accounts they
+contain.  When providing this field, the account directories under
+@file{/var/lib/jami/} are recreated every time the service starts,
+ensuring a consistent state.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@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/local.mk b/gnu/local.mk
index dd68bb5957..18700f78a1 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -712,6 +712,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
@@ -1842,7 +1843,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..d0dc79a68a 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  <nee-git <at> hidamari.blue>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -17,16 +18,31 @@
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (define-module (gnu services telephony)
-  #:use-module (gnu services)
+  #: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 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 (ice-9 match)
-  #:export (murmur-configuration
+  #:export (jami-daemon-configuration
+            jami-daemon-configuration-jami-daemon
+            jami-daemon-configuration-dbus
+            jami-daemon-configuration-enable-logging?
+            jami-daemon-configuration-debug?
+            jami-daemon-configuration-auto-answer?
+            jami-daemon-configuration-account-archives
+
+            jami-daemon-service-type
+
+            murmur-configuration
             make-murmur-configuration
             murmur-configuration?
             murmur-configuration-package
@@ -74,6 +90,337 @@
 
             murmur-service-type))
 
+
+
+;;;
+;;; Jami daemon.
+;;;
+
+;;; Copied from (gnu services messaging).
+(define (string-list? val)
+  (and (list? val)
+       (and-map (lambda (x)
+                  (or (computed-file? x) ;XXX: for tests
+                      (and (string? x) (not (string-index x #\,)))))
+                val)))
+
+(define-maybe/no-serialization string-list)
+
+(define-configuration/no-serialization jami-daemon-configuration
+  (jami-daemon
+   (package libring)
+   "The Jami daemon package to use.")
+  (dbus
+   (package dbus)
+   "The D-Bus package to use to start the required D-Bus session.")
+  (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.")
+  (account-archives
+   (maybe-string-list 'disabled)
+   "A list of Jami account archive (backup) file names to be (re-)provisioned
+every time the Jami daemon service starts.  These Jami account backups should
+@emph{not} be encrypted and should be made readable only to the @samp{jami}
+user (i.e., not in the store), to guard against leaking the secret key
+material of the Jami accounts they contain.  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-daemon-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-daemon-configuration->command-line-arguments config)
+  "Derive the command line arguments to used to launch the Jami daemon from
+CONFIG, a <jami-daemon-configuration> object."
+  (match-record config <jami-daemon-configuration>
+    (jami-daemon dbus enable-logging? debug? auto-answer?)
+    `(,(file-append jami-daemon "/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)))))
+
+;; Local definitions to expand in source form in G-exps.
+(define define-with-retries
+  '(define-syntax-rule (with-retries n delay body ...)
+     "Retry the code in BODY up to N times until it returns #t,
+else #f.  A delay of DELAY seconds is inserted before each retry."
+     (let loop ((attempts 0))
+       (if (< attempts n)
+           (or (catch #t
+                 (lambda ()
+                   body ...)
+                 (lambda args
+                   #f))
+               (begin
+                 (sleep delay)          ;else wait and retry
+                 (loop (+ 1 attempts))))
+           (error "maximum number of retry attempts reached")))))
+
+(define define-send-dbus
+  '(define (send-dbus dbus-send service path interface method . arguments)
+     "Return the response of dbus-send, else #f."
+     (let* ((command `(,dbus-send
+                       "--bus=unix:path=/var/run/jami/bus"
+                       "--print-reply"
+                       ,(string-append "--dest=" service) ;e.g., cx.ring.Ring
+                       ,path         ;e.g., /cx/ring/Ring/ConfigurationManager
+                       ,(string-append interface "." method)
+                       ,@arguments))
+            (temporary-log-file "/var/run/jami/.temporary-dbus-output.log")
+            (clear-temp-file (lambda () (false-if-exception
+                                         (delete-file temporary-log-file)))))
+       (catch #t
+         (lambda ()
+           (dynamic-wind
+             clear-temp-file
+             (lambda ()
+               (let ((pid (fork+exec-command command
+                                             #:user "jami" #:group "jami"
+                                             #:log-file temporary-log-file)))
+                 (match (waitpid pid)
+                   ((_ . status)
+                    (let ((exit-status (status:exit-val status)))
+                      (unless (= 0 exit-status)
+                        (error "the send-dbus command exited with"
+                               exit-status))
+                      #t))))
+               (call-with-input-file temporary-log-file get-string-all))
+             clear-temp-file))
+         (lambda args
+           (format (current-error-port) "command ~s failed with ~a~%"
+                   command args)
+           #f)))))
+
+(define (jami-daemon-shepherd-services config)
+  "Return a <shepherd-service> running the Jami daemon."
+  (let* ((jami-daemon (jami-daemon-configuration-jami-daemon config))
+         (dbus (jami-daemon-configuration-dbus config))
+         (dbus-daemon (file-append dbus "/bin/dbus-daemon"))
+         (dbus-send (file-append dbus "/bin/dbus-send"))
+         (accounts (jami-daemon-configuration-account-archives config))
+         (declarative-mode? (not (eq? 'disabled accounts))))
+
+    (with-imported-modules (source-module-closure
+                            '((gnu build shepherd)
+                              (gnu system file-systems)))
+      (list (shepherd-service
+             (documentation "Run a D-Bus session for the Jami daemon.")
+             (provision '(jami-daemon-dbus-session))
+             (modules `((gnu build shepherd)
+                        (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-with-retries
+
+                  (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="
+                                           #$jami-daemon "/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 <pidfile> 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-daemon dring))
+             (requirement '(jami-daemon-dbus-session))
+             (modules `((ice-9 ftw)
+                        (ice-9 match)
+                        (rnrs io ports)
+                        (srfi srfi-1)
+                        (srfi srfi-26)
+                        (gnu build shepherd)
+                        (gnu system file-systems)
+                        ,@%default-modules))
+             (start
+              #~(lambda args
+                  #$define-with-retries
+                  #$define-send-dbus
+
+                  (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 accounts 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)))
+                                '#$accounts)))
+
+                  ;; Start the daemon.
+                  (define daemon-pid
+                    ((make-forkexec-constructor/container
+                      '#$(jami-daemon-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)))
+                      #:user "jami"
+                      #:group "jami"
+                      #:environment-variables
+                      (list (string-append "DBUS_SESSION_BUS_ADDRESS="
+                                           "unix:path=/var/run/jami/bus")))))
+
+                  ;; Wait until the service name has been acquired by D-Bus
+                  ;; (this does *not* trigger automatic D-Bus service
+                  ;; activation, which is what we want).
+                  (with-retries 20 1
+                    (let ((output (send-dbus #$dbus-send "org.freedesktop.DBus"
+                                             "/org/freedesktop/DBus"
+                                             "org.freedesktop.DBus"
+                                             "ListNames")))
+                      (string-contains output "cx.ring.Ring")))
+
+                  ;; Provision the accounts, by means of D-Bus commands sent to
+                  ;; the daemon.
+                  (when #$declarative-mode?
+                    (or (every
+                         identity
+                         (map (lambda (archive)
+                                (send-dbus
+                                 #$dbus-send
+                                 "cx.ring.Ring"
+                                 "/cx/ring/Ring/ConfigurationManager"
+                                 "cx.ring.Ring.ConfigurationManager"
+                                 "addAccount"
+                                 (string-append
+                                  "dict:string:string:Account.archivePath,"
+                                  archive
+                                  ",Account.type,RING")))
+                              (map (cut string-append
+                                        "/var/lib/jami/accounts/" <>)
+                                   (scandir "/var/lib/jami/accounts/"
+                                            (lambda (f)
+                                              (not (member f '("." ".."))))))))
+                        (error "failed provisioning the jami accounts")))
+
+                  ;; Finally, return the PID of the dring process.
+                  daemon-pid))
+             (stop
+              #~(lambda (pid . args)
+                  (kill pid SIGTERM)
+                  ;; Wait for the process to exit; this prevents overlapping
+                  ;; processes when issuing 'herd restart'.
+                  (waitpid pid)
+                  #f)))))))
+
+(define jami-daemon-service-type
+  (service-type
+   (name 'jami-daemon)
+   (default-value (jami-daemon-configuration))
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             jami-daemon-shepherd-services)
+          (service-extension account-service-type
+                             (const %jami-daemon-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> murmur-configuration
@@ -305,3 +652,7 @@ suite.")
                        (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..026890052d
--- /dev/null
+++ b/gnu/tests/data/jami-dummy-account.dat
@@ -0,0 +1,391 @@
+;;; 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..1d98294411
--- /dev/null
+++ b/gnu/tests/telephony.scm
@@ -0,0 +1,204 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer <at> gnu.org>.
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(define-module (gnu tests telephony)
+  #:use-module (gnu)
+  #: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 ssh)
+  #:use-module (gnu services telephony)
+  #:use-module (guix gexp)
+  #:use-module (guix modules)
+  #:export (%test-jami-daemon
+            %test-jami-daemon-provisioning))
+
+;;;
+;;; Jami daemon.
+;;;
+
+(define define-with-retries (@@ (gnu services telephony) define-with-retries))
+
+(include "data/jami-dummy-account.dat") ;defines %jami-account-content-sexp
+
+(define %dummy-jami-account
+  ;; 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* (make-jami-daemon-os #:key provisioning?)
+  (simple-operating-system
+   (service jami-daemon-service-type
+            (if provisioning?
+                (jami-daemon-configuration
+                 (account-archives (list %dummy-jami-account)))
+                (jami-daemon-configuration)))
+   (service dbus-root-service-type)
+   (service openssh-service-type        ;for debugging
+            (openssh-configuration
+             (permit-root-login 'without-password)))))
+
+(define %jami-daemon-os
+  (make-jami-daemon-os))
+
+(define %jami-daemon-os-provisioning
+  (make-jami-daemon-os #:provisioning? #t))
+
+(define* (run-jami-daemon-test #:key provisioning?)
+  "Run tests in %JAMI-DAEMON-OS.  When PROVISIONING? is true, test the
+accounts provisioning feature of the service."
+  (define os (marionette-operating-system
+              (if provisioning?
+                  %jami-daemon-os-provisioning
+                  %jami-daemon-os)
+              #:imported-modules '((gnu services herd)
+                                   (guix combinators))))
+  (define vm (virtual-machine
+              (operating-system os)
+              (memory-size 512)))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (rnrs base)
+                       (srfi srfi-11)
+                       (srfi srfi-64)
+                       (gnu build marionette))
+
+          (define marionette
+            (make-marionette (list #$vm)))
+
+          (mkdir #$output)
+          (chdir #$output)
+
+          (test-begin "jami-daemon")
+
+          (test-assert "service running"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+                (match (start-service 'jami-daemon)
+                  (#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))
+                #$define-with-retries
+                (setenv "PATH" "/run/current-system/profile/bin")
+                (let ((pid (match (start-service 'jami-daemon)
+                             (#f #f)
+                             (('service response-parts ...)
+                              (match (assq-ref response-parts 'running)
+                                ((pid) pid))))))
+
+                  (assert (number? pid))
+
+                  (match (stop-service 'jami-daemon)
+                    (services           ;a list of service symbols
+                     (member 'jami-daemon 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-daemon)
+                              (#f #f)
+                              (('service response-parts ...)
+                               (match (assq-ref response-parts 'running)
+                                 ((pid) pid)))))
+                (assert (number? pid))
+
+                ;; Restart the service.
+                (restart-service 'jami-daemon)
+
+                (define new-pid (match (start-service 'jami-daemon)
+                                  (#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"
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd))
+
+                #$define-with-retries
+
+                (setenv "PATH" "/run/current-system/profile/bin")
+                ;; The imported account takes some time to appear in the
+                ;; account files.
+                (with-retries 20 1
+                  (zero? (status:exit-val
+                          (system* "grep" "-r"
+                                   #$(assoc-ref %jami-account-content-sexp
+                                                "Account.username")
+                                   "/var/lib/jami/.local/share/jami/")))))
+             marionette))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+
+  (gexp->derivation (if provisioning?
+                        "jami-daemon-provisioning-test"
+                        "jami-daemon-test")
+                    test))
+
+(define %test-jami-daemon
+  (system-test
+   (name "jami-daemon")
+   (description "Basic tests for the jami-daemon service.")
+   (value (run-jami-daemon-test))))
+
+(define %test-jami-daemon-provisioning
+  (system-test
+   (name "jami-daemon-provisioning")
+   (description "Provisioning test for the jami-daemon service.")
+   (value (run-jami-daemon-test #:provisioning? #t))))
+
+;; Local Variables:
+;; eval: (put 'with-retries 'scheme-indent-function 2)
+;; End:
-- 
2.31.1





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Thu, 20 May 2021 12:38:02 GMT) Full text and rfc822 format available.

Message #32 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Maxime Devos <maximedevos <at> telenet.be>
Cc: 47849 <at> debbugs.gnu.org
Subject: Re: [bug#47849] [PATCH 1/1] services: Add a service for the Jami
 daemon.
Date: Thu, 20 May 2021 08:37:19 -0400
Hello Maxime.

Maxime Devos <maximedevos <at> telenet.be> writes:

> Maxim Cournoyer schreef op za 17-04-2021 om 16:06 [-0400]:
>> +                ;; Start the daemon.
>> +                (define daemon-pid
>> +                  (fork+exec-command
>> +                   '#$(jami-daemon-configuration->command-line-arguments config)
>> +                   #:user "jami"
>> +                   #:group "jami"
>> +                   #:environment-variables
>> +                   (list (string-append "DBUS_SESSION_BUS_ADDRESS="
>> +                                        "unix:path=/var/run/jami/bus"))))
>
> It would be nice if this could be run in a container
> that only has access to the relevant parts of the file system
> (and not, say, /run/setuid-programs).  See, e.g., gnu/build/linux-container.scm.

That's now the case in the just-sent v2, both for the D-Bus session
service as well as the Jami process itself :-).  I figured out I could
simply call make-forkexec+constructor/container and execute apply the
resulting procedure.

I've also manage to (with much difficulty!) have the service properly
start, stop or restart without races.  The new tests proved really
useful for the lengthy trial and error process that I had to go through.

Thanks for your patience!

Maxim




Changed bug title to '[PATCH] Add a jami-daemon service.' from '[PATCH 0/1] [preview] Add a jami-daemon service.' Request was from Maxim Cournoyer <maxim.cournoyer <at> gmail.com> to control <at> debbugs.gnu.org. (Thu, 20 May 2021 14:50:01 GMT) Full text and rfc822 format available.

Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 01 Aug 2021 07:00:02 GMT) Full text and rfc822 format available.

Message #37 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: 47849 <at> debbugs.gnu.org
Cc: Maxime Devos <maximedevos <at> telenet.be>
Subject: [PATCH v3] services: Add a service for the Jami daemon.
Date: Sun,  1 Aug 2021 02:58:54 -0400
Hello!

This is a much more featureful/refined version of the previously sent v2 Jami
service.  I think it's ready!

Thank you,

Maxim

[PATCH v3 1/3] .dir-locals.el: Specify indentation rule for
[PATCH v3 2/3] build: shepherd: Use autoload to lazily bind Shepherd
[PATCH v3 3/3] services: Add a service for Jami.





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 01 Aug 2021 07:00:02 GMT) Full text and rfc822 format available.

Message #40 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: 47849 <at> debbugs.gnu.org
Cc: Maxime Devos <maximedevos <at> telenet.be>,
 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: [PATCH v3 1/3] .dir-locals.el: Specify indentation rule for
 with-shepherd-action.
Date: Sun,  1 Aug 2021 02:58:55 -0400
* .dir-locals.el (scheme-mode) <with-shepherd-action>: New indentation rule.
---
 .dir-locals.el | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.dir-locals.el b/.dir-locals.el
index a4fcbfe7ca..aaa48ab552 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -141,6 +141,8 @@
 
    (eval . (put 'with-paginated-output-port 'scheme-indent-function 1))
 
+   (eval . (put 'with-shepherd-action 'scheme-indent-function 3))
+
    ;; This notably allows '(' in Paredit to not insert a space when the
    ;; preceding symbol is one of these.
    (eval . (modify-syntax-entry ?~ "'"))
-- 
2.32.0





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 01 Aug 2021 07:00:03 GMT) Full text and rfc822 format available.

Message #43 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: 47849 <at> debbugs.gnu.org
Cc: Maxime Devos <maximedevos <at> telenet.be>,
 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: [PATCH v3 2/3] build: shepherd: Use autoload to lazily bind Shepherd
 modules.
Date: Sun,  1 Aug 2021 02:58:56 -0400
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(-)

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 @@
   #: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 @@
                            (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. "
-- 
2.32.0





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 01 Aug 2021 07:00:03 GMT) Full text and rfc822 format available.

Message #46 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: 47849 <at> debbugs.gnu.org
Cc: Maxime Devos <maximedevos <at> telenet.be>,
 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: [PATCH v3 3/3] services: Add a service for Jami.
Date: Sun,  1 Aug 2021 02:58:57 -0400
* 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                         | 264 ++++++++++
 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, 2742 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

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..c931cbeb93 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -22526,6 +22526,270 @@ 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)
+Available @code{jami-configuration} fields are:
+
+@deftypevr {@code{jami-configuration} parameter} package jamid
+The Jami daemon package to use.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} package dbus
+The D-Bus package to use to start the required D-Bus session.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} package nss-certs
+The nss-certs package to use to provide TLS certificates.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} boolean enable-logging?
+Whether to enable logging to syslog.
+
+Defaults to @samp{#t}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} boolean debug?
+Whether to enable debug level messages.
+
+Defaults to @samp{#f}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} boolean auto-answer?
+Whether to force automatic answer to incoming calls.
+
+Defaults to @samp{#f}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-configuration} parameter} maybe-jami-account-list accounts
+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.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@c Auto-generated via (configuration->documentation 'jami-account)
+Available @code{jami-account} fields are:
+
+@deftypevr {@code{jami-account} parameter} string-or-computed-file archive
+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.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-account-fingerprint-list allowed-contacts
+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.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-account-fingerprint-list moderators
+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.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-boolean rendezvous-point?
+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.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-boolean peer-discovery?
+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.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-string-list bootstrap-hostnames
+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.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@deftypevr {@code{jami-account} parameter} maybe-string name-server-uri
+The URI of the name server to use, that can be used to retrieve the
+account fingerprint for a registered username.
+
+Defaults to @samp{disabled}.
+
+@end deftypevr
+
+@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..bb43b578f9
--- /dev/null
+++ b/gnu/build/jami-service.scm
@@ -0,0 +1,587 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+;;; 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
+                          (list dbus-send)
+                          (list (%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 c530507b1a..0f094e580b 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
@@ -1879,7 +1881,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  <nee-git <at> hidamari.blue>
+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -17,16 +18,45 @@
 ;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
 
 (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 @@
 
             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 <jami-account> 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 <jami-configuration> object."
+  (match-record config <jami-configuration>
+    (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 <shepherd-service> 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 <pidfile> 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> murmur-configuration
@@ -305,3 +981,7 @@ suite.")
                        (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 <maxim.cournoyer <at> gnu.org>.
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(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 <maxim.cournoyer <at> gmail.com>
+;;;
+;;; 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 <http://www.gnu.org/licenses/>.
+
+(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)
-- 
2.32.0





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Sun, 01 Aug 2021 07:59:01 GMT) Full text and rfc822 format available.

Message #49 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Leo Prikler <leo.prikler <at> student.tugraz.at>
To: Maxime Devos <maximedevos <at> telenet.be>, 47849 <at> debbugs.gnu.org
Cc: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Subject: Re: [PATCH v3 3/3] services: Add a service for Jami.
Date: Sun, 01 Aug 2021 09:58:02 +0200
Hi,

Am Sonntag, den 01.08.2021, 02:58 -0400 schrieb Maxim Cournoyer:
> [...]

> +  (let* ((command `(,@(if dbus-send
> +                          (list dbus-send)
> +                          (list (%send-dbus-binary)))
You can use non-splicing comma notation for things that are never
supposed to be lists.
> +                    ,@(if (or bus (%send-dbus-bus))
> +                          (list (string-append "--bus="
> +                                               (or bus (%send-dbus-
> bus))))
> +                          '())
You use this style here, but
> +        (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)))
this style here.  I personally think using and=> everywhere would make
things a little clearer.

More importantly, though
> +(define* (send-dbus #:key service path interface method
> +                    bus
> +                    dbus-send
> +                    user group
> +                    timeout
> +                    arguments)
If you e.g. write (bus (%send-dbus-bus)), I think the argument should
already be correctly passed upon the function call, no?

> +          (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)))))))
Since output is unused in the error case, I think you can move the get-
string-all there.  It would make a difference if you were to e.g. close
temp-port before that.

I did not look at the rest of this patch, but 1+2 LGTM.

Regards,





Reply sent to Maxim Cournoyer <maxim.cournoyer <at> gmail.com>:
You have taken responsibility. (Mon, 02 Aug 2021 19:18:02 GMT) Full text and rfc822 format available.

Notification sent to Maxim Cournoyer <maxim.cournoyer <at> gmail.com>:
bug acknowledged by developer. (Mon, 02 Aug 2021 19:18:02 GMT) Full text and rfc822 format available.

Message #54 received at 47849-done <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Leo Prikler <leo.prikler <at> student.tugraz.at>
Cc: Maxime Devos <maximedevos <at> telenet.be>, 47849-done <at> debbugs.gnu.org
Subject: Re: bug#47849: [PATCH] Add a jami-daemon service.
Date: Mon, 02 Aug 2021 15:17:17 -0400
Hi Leo,

Leo Prikler <leo.prikler <at> student.tugraz.at> writes:

> Hi,
>
> Am Sonntag, den 01.08.2021, 02:58 -0400 schrieb Maxim Cournoyer:
>> [...]
>
>> +  (let* ((command `(,@(if dbus-send
>> +                          (list dbus-send)
>> +                          (list (%send-dbus-binary)))
> You can use non-splicing comma notation for things that are never
> supposed to be lists.

Good catch!

>> +                    ,@(if (or bus (%send-dbus-bus))
>> +                          (list (string-append "--bus="
>> +                                               (or bus (%send-dbus-
>> bus))))
>> +                          '())
> You use this style here, but
>> +        (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)))
> this style here.  I personally think using and=> everywhere would make
> things a little clearer.

I tried, but it seemed it had a bigger cognitive load to follow (or,
and=>, compose, list, cut all in the same expression), so ended up
keeping it as is.

> More importantly, though
>> +(define* (send-dbus #:key service path interface method
>> +                    bus
>> +                    dbus-send
>> +                    user group
>> +                    timeout
>> +                    arguments)
> If you e.g. write (bus (%send-dbus-bus)), I think the argument should
> already be correctly passed upon the function call, no?

the %send-dbus-bus is a parameter; if we were to provide it as a default
value, it would be evaluated at the time the module is read (IIUC), but
I want its value to be read at run time, when the procedure is called
instead.

>> +          (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)))))))
> Since output is unused in the error case, I think you can move the get-
> string-all there.  It would make a difference if you were to e.g. close
> temp-port before that.

It's actually used as the last argument of the 'error' call :-).  I
added it as it was helpful to debug stderr messages from dbus-send.

> I did not look at the rest of this patch, but 1+2 LGTM.
>
> Regards,

Thanks a lot for having a look!  It's been long in the making.

Pushed as commit 69dcc24c9f.

Closing.

Maxim




Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Tue, 03 Aug 2021 07:36:01 GMT) Full text and rfc822 format available.

Message #57 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Leo Prikler <leo.prikler <at> student.tugraz.at>
To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Cc: 47849 <at> debbugs.gnu.org, Maxime Devos <maximedevos <at> telenet.be>
Subject: Re: bug#47849: [PATCH] Add a jami-daemon service.
Date: Tue, 03 Aug 2021 09:35:22 +0200
Hi Maxim,

Am Montag, den 02.08.2021, 15:17 -0400 schrieb Maxim Cournoyer:
> > > +                    ,@(if (or bus (%send-dbus-bus))
> > > +                          (list (string-append "--bus="
> > > +                                               (or bus (%send-
> > > dbus-
> > > bus))))
> > > +                          '())
> > You use this style here, but
> > > +        (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)))
> > this style here.  I personally think using and=> everywhere would
> > make
> > things a little clearer.
> 
> I tried, but it seemed it had a bigger cognitive load to follow (or,
> and=>, compose, list, cut all in the same expression), so ended up
> keeping it as is.
I think the cognitive burden would in both cases be lessened if we
didn't need to write (or stuff (%send-dbus-stuff)) everywhere, which
see below.

> > More importantly, though
> > > +(define* (send-dbus #:key service path interface method
> > > +                    bus
> > > +                    dbus-send
> > > +                    user group
> > > +                    timeout
> > > +                    arguments)
> > If you e.g. write (bus (%send-dbus-bus)), I think the argument
> > should
> > already be correctly passed upon the function call, no?
> 
> the %send-dbus-bus is a parameter; if we were to provide it as a
> default
> value, it would be evaluated at the time the module is read (IIUC),
> but
> I want its value to be read at run time, when the procedure is called
> instead.
That's how define* works though, in my experience:

--8<---------------cut here---------------start------------->8---
;; foo.scm
(define-module (foo)
  #:export (*rand* rand))

(define *rand* (make-parameter 4))
(define* (rand #:optional (value (*rand*))) value)
--8<---------------cut here---------------end--------------->8---

--8<---------------cut here---------------start------------->8---
scheme@(guile-user)> ,use (foo) ;; alternatively (load-compiled
"foo.go")
scheme@(guile-user)> (rand)
$1 = 4
scheme@(guile-user)> (parameterize ((*rand* 5)) (rand))
$2 = 5
scheme@(guile-user)> (rand 6)
$3 = 6
--8<---------------cut here---------------end--------------->8---

> > > +          (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)))))))
> > Since output is unused in the error case, I think you can move the
> > get-
> > string-all there.  It would make a difference if you were to e.g.
> > close
> > temp-port before that.
> 
> It's actually used as the last argument of the 'error' call :-).  I
> added it as it was helpful to debug stderr messages from dbus-send.
My bad, nvm then.

> > I did not look at the rest of this patch, but 1+2 LGTM.
> > 
> > Regards,
> 
> Thanks a lot for having a look!  It's been long in the making.
> 
> Pushed as commit 69dcc24c9f.
As I said, I only looked at parts of it, so this push was perhaps a bit
hasty.  I recently also hastily pushed something and had to fix it up
later.  Let's try to keep a cool head and not let the heat get to us :)

Regards,





Information forwarded to guix-patches <at> gnu.org:
bug#47849; Package guix-patches. (Tue, 03 Aug 2021 14:37:01 GMT) Full text and rfc822 format available.

Message #60 received at 47849 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Leo Prikler <leo.prikler <at> student.tugraz.at>
Cc: 47849 <at> debbugs.gnu.org, Maxime Devos <maximedevos <at> telenet.be>
Subject: Re: bug#47849: [PATCH] Add a jami-daemon service.
Date: Tue, 03 Aug 2021 10:36:45 -0400
Hi Leo,

[...]

>> > I did not look at the rest of this patch, but 1+2 LGTM.
>> > 
>> > Regards,
>> 
>> Thanks a lot for having a look!  It's been long in the making.
>> 
>> Pushed as commit 69dcc24c9f.
> As I said, I only looked at parts of it, so this push was perhaps a bit
> hasty.  I recently also hastily pushed something and had to fix it up
> later.  Let's try to keep a cool head and not let the heat get to us
> :)

Point taken; there was an adjustment to be made in (guix self) that I
had overlooked; the push ended up breaking 'guix pull' (I quickly
reverted and fixed it with the help of Chris Baines -- thank you!).
Letting some more time would have perhaps allowed someone to catch that
issue (or running 'make as-derivation').

Thanks again,

Maxim




bug archived. Request was from Debbugs Internal Request <help-debbugs <at> gnu.org> to internal_control <at> debbugs.gnu.org. (Wed, 01 Sep 2021 11:24:08 GMT) Full text and rfc822 format available.

This bug report was last modified 2 years and 230 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.