Subject: [PATCH] gnu: Add webssh service.
From: Oleg Pykhalov <[email-masked]>
To: [email-masked]
Cc: Oleg Pykhalov <[email-masked]>
Date: Tue, 29 Sep 2020 00:01:58 +0300
List-Id: <debbugs-submit.debbugs.gnu.org>

* gnu/services/ssh.scm:
(<webssh-configuration>): New record.
(%webssh-configuration-nginx, webssh-service-type): New variables.
(webssh-account, webssh-activation, webssh-shepherd-service): New procedures.
* doc/guix.texi: Document this.
---
 doc/guix.texi        |  75 +++++++++++++++++++++++++
 gnu/services/ssh.scm | 131 ++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 205 insertions(+), 1 deletion(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 452453241c..f745b04bb9 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -15546,6 +15546,81 @@ may cause undefined behaviour.
 @end table
 @end deftp
 
+@cindex WebSSH
+@deffn {Scheme Variable} webssh-service-type
+This is the type for the @uref{https://webssh.huashengdun.org/, WebSSH}
+program that runs a web SSH client.  WebSSH can be run manually from the
+command-line by passing arguments to the binary @command{wssh} from the
+package @code{webssh}, but it can also be run as a Guix service.  This
+latter use case is documented here.
+
+For example, to specify a service running WebSSH on loopback interface
+on port @code{8888} with reject policy with a list of allowed to
+connection hosts, and NGINX as a reverse-proxy to this service listening
+for HTTPS connection, add this call to the operating system's
+@code{services} field:
+
+@lisp
+(service webssh-service-type
+  (webssh-configuration (address "127.0.0.1")
+                        (port 8888)
+                        (policy 'reject)
+                        (known-hosts '("localhost ecdsa-sha2-nistp256 AAAA…"
+                                       "127.0.0.1 ecdsa-sha2-nistp256 AAAA…"))))
+
+(service nginx-service-type
+         (nginx-configuration
+          (server-blocks
+           (list
+            (nginx-server-configuration
+             (inherit %webssh-configuration-nginx)
+             (server-name '("webssh.example.com"))
+             (listen '("443 ssl"))
+             (ssl-certificate (letsencrypt-certificate "webssh.example.com"))
+             (ssl-certificate-key (letsencrypt-key "webssh.example.com"))
+             (locations
+              (cons (nginx-location-configuration
+                     (uri "/.well-known")
+                     (body '("root /var/www;")))
+                    (nginx-server-configuration-locations %webssh-configuration-nginx))))))))
+@end lisp
+@end deffn
+
+@deftp {Data Type} webssh-configuration
+Data type representing the configuration for @code{webssh-service}.
+
+@table @asis
+@item @code{package} (default: @var{webssh})
+@code{webssh} package to use.
+
+@item @code{user-name} (default: @var{"webssh"})
+User name or user ID that file transfers to and from that module should take
+place.
+
+@item @code{group-name} (default: @var{"webssh"})
+Group name or group ID that will be used when accessing the module.
+
+@item @code{address} (default: @var{#f})
+IP address on which @command{webssh} listens for incoming connections.
+
+@item @code{port} (default: @var{8888})
+TCP port on which @command{webssh} listens for incoming connections.
+
+@item @code{policy} (default: @var{#f})
+Connection policy.  @var{reject} policy requires to specify @var{known-hosts}.
+
+@item @code{known-hosts} (default: @var{'()})
+List of hosts which allowed for SSH connection from @command{webssh}.
+
+@item @code{log-file} (default: @file{"/var/log/webssh.log"})
+Name of the file where @command{rsync} writes its log file.
+
+@item @code{log-level} (default: @var{#f})
+Logging level.
+
+@end table
+@end deftp
+
 @defvr {Scheme Variable} %facebook-host-aliases
 This variable contains a string for use in @file{/etc/hosts}
 (@pxref{Host Names,,, libc, The GNU C Library Reference Manual}).  Each
diff --git a/gnu/services/ssh.scm b/gnu/services/ssh.scm
index ced21c0742..1891db0487 100644
--- a/gnu/services/ssh.scm
+++ b/gnu/services/ssh.scm
@@ -5,6 +5,7 @@
 ;;; Copyright © 2017 Clément Lassieur <[email-masked]>
 ;;; Copyright © 2019 Ricardo Wurmus <[email-masked]>
 ;;; Copyright © 2020 pinoaffe <[email-masked]>
+;;; Copyright © 2020 Oleg Pykhalov <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -26,6 +27,7 @@
   #:use-module (gnu packages admin)
   #:use-module (gnu services)
   #:use-module (gnu services shepherd)
+  #:use-module (gnu services web)
   #:use-module (gnu system pam)
   #:use-module (gnu system shadow)
   #:use-module (guix gexp)
@@ -50,7 +52,12 @@
 
             autossh-configuration
             autossh-configuration?
-            autossh-service-type))
+            autossh-service-type
+
+            webssh-configuration
+            webssh-configuration?
+            webssh-service-type
+            %webssh-configuration-nginx))
 
 ;;; Commentary:
 ;;;
@@ -732,4 +739,126 @@ object."
                              autossh-service-activation)))
    (default-value (autossh-configuration))))
 
+
+;;;
+;;; WebSSH
+;;;
+
+(define-record-type* <webssh-configuration>
+  webssh-configuration make-webssh-configuration
+  webssh-configuration?
+  (package     webssh-configuration-package     ;package
+               (default webssh))
+  (user-name   webssh-configuration-user-name   ;string
+               (default "webssh"))
+  (group-name  webssh-configuration-group-name  ;string
+               (default "webssh"))
+  (policy      webssh-configuration-policy      ;symbol
+               (default #f))
+  (known-hosts webssh-configuration-known-hosts ;list of strings
+               (default #f))
+  (port        webssh-configuration-port        ;number
+               (default #f))
+  (address     webssh-configuration-address     ;string
+               (default #f))
+  (log-file    webssh-configuration-log-file    ;string
+               (default "/var/log/webssh.log"))
+  (log-level   webssh-configuration-log-level   ;symbol
+               (default #f)))
+
+(define %webssh-configuration-nginx
+  (nginx-server-configuration
+   (listen '("80"))
+   (locations
+    (list (nginx-location-configuration
+           (uri "/")
+           (body '("proxy_pass http://127.0.0.1:8888;"
+                   "proxy_http_version 1.1;"
+                   "proxy_read_timeout 300;"
+                   "proxy_set_header Upgrade $http_upgrade;"
+                   "proxy_set_header Connection \"upgrade\";"
+                   "proxy_set_header Host $http_host;"
+                   "proxy_set_header X-Real-IP $remote_addr;"
+                   "proxy_set_header X-Real-PORT $remote_port;")))))))
+
+(define webssh-account
+  ;; Return the user accounts and user groups for CONFIG.
+  (match-lambda
+    (($ <webssh-configuration> _ user-name group-name _ _ _ _ _ _)
+     (list (user-group
+            (name group-name))
+           (user-account
+            (name user-name)
+            (group group-name)
+            (comment "webssh privilege separation user")
+            (home-directory (string-append "/var/run/" user-name))
+            (shell #~(string-append #$shadow "/sbin/nologin")))))))
+
+(define webssh-activation
+  ;; Return the activation GEXP for CONFIG.
+  (match-lambda
+    (($ <webssh-configuration> _ user-name group-name policy known-hosts _ _
+                               log-file _)
+     (with-imported-modules '((guix build utils))
+       #~(begin
+           (let* ((home-dir (string-append "/var/run/" #$user-name))
+                  (ssh-dir (string-append home-dir "/.ssh"))
+                  (known-hosts-file (string-append ssh-dir "/known_hosts")))
+             (call-with-output-file #$log-file (const #t))
+             (mkdir-p ssh-dir)
+             (case '#$policy
+               ((reject)
+                (if '#$known-hosts
+                    (call-with-output-file known-hosts-file
+                      (lambda (port)
+                        (for-each (lambda (host) (display host port) (newline port))
+                                  '#$known-hosts)))
+                    (display-hint (G_ "webssh: reject policy requires `known-hosts'.")))))
+             (for-each (lambda (file)
+                         (chown file
+                                (passwd:uid (getpw #$user-name))
+                                (group:gid (getpw #$group-name))))
+                       (list #$log-file ssh-dir known-hosts-file))
+             (chmod ssh-dir #o700)))))))
+
+(define webssh-shepherd-service
+  (match-lambda
+    (($ <webssh-configuration> package user-name group-name policy _ port
+                               address log-file log-level)
+     (list (shepherd-service
+            (provision '(webssh))
+            (documentation "Run webssh daemon.")
+            (start #~(make-forkexec-constructor
+                      `(,(string-append #$webssh "/bin/wssh")
+                        ,(string-append "--log-file-prefix=" #$log-file)
+                        ,@(case '#$log-level
+                            ((debug) '("--logging=debug"))
+                            (else '()))
+                        ,@(case '#$policy
+                            ((reject) '("--policy=reject"))
+                            (else '()))
+                        ,@(if #$port
+                              (list (string-append "--port=" (number->string #$port)))
+                              '())
+                        ,@(if #$address
+                              (list (string-append "--address=" #$address))
+                              '()))
+                      #:user #$user-name
+                      #:group #$group-name))
+            (stop #~(make-kill-destructor)))))))
+
+(define webssh-service-type
+  (service-type
+   (name 'webssh)
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             webssh-shepherd-service)
+          (service-extension account-service-type
+                             webssh-account)
+          (service-extension activation-service-type
+                             webssh-activation)))
+   (default-value (webssh-configuration))
+   (description
+    "Run the webssh.")))
+
 ;;; ssh.scm ends here
-- 
2.28.0