aboutsummaryrefslogtreecommitdiff
;; SPDX-License-Identifier: CC0-1.0

;; Copyright (C) 2022-2024 Wojtek Kosior <koszko@koszko.org>
;;
;; Available under the terms of Creative Commons Zero v1.0 Universal.

(use-modules ((srfi srfi-1) #:select (append-map filter-map))
             (gnu)
             (koszko-org-website)
             (sheets-websites)
             (hydrilla-website)
             (hydrilla-json-schemas)
             (hydrilla-base)
             (ice-9 match)
             ;; srfi-26 provides `cut`.
             (srfi srfi-26)
             (guix records)
             ;; (guix gexp) is needed for `file-append`.
             (guix gexp)
             ;; The following 4 are needed to construct GUIX_PYTHONPATH for
             ;; Hydrilla WSGI scripts.
             (guix build-system python)
             (guix packages)
             (guix search-paths)
             (guix modules)
             ((guix utils) #:select (substitute-keyword-arguments))
             ;; The following exports account-service-type.
             (gnu system shadow)
             ((gnu system setuid) #:select (setuid-program)))
(use-package-modules web
                     python
                     version-control
                     mail
                     admin
                     tls)
(use-service-modules web
                     shepherd
                     certbot
                     mail
                     dns)

(define %here
  (getcwd))

(define* (g-string-join elements #:optional (joiner " "))
  #~(string-join (list #$@elements) #$joiner))

(define* (g-string-append #:rest args)
  #~(string-append #$@args))

(define g-symlink-exists?
  #~(lambda (path)
      (false-if-exception
       (with-input-from-file (dirname path)
         (lambda _
           (statat (current-input-port)
                   (basename path) AT_SYMLINK_NOFOLLOW))))))

(define (httpd-conf-token arg)
  (match arg
    ((? string?)
     (if (or-map (cut string-contains arg <>) '(" " "\""))
         (format #f "~s" arg)
         arg))
    ((? symbol?)
     (httpd-conf-token (symbol->string arg)))
    (_
     #~(let ((gexp-value #$arg))
         (if (string-contains gexp-value " ")
             (format #f "~s" gexp-value)
             gexp-value)))))

(define* (httpd-directive name #:rest args)
  #~(format #f "~a~%"
            #$(g-string-join (map httpd-conf-token (cons name args)))))

(define* (httpd-tag name args #:rest body)
  (let ((tag-name (httpd-conf-token name)))
    #~(format #f "<~a ~a>~%~a</~a>~%"
              #$tag-name
              #$(g-string-join (map httpd-conf-token args))
              #$(apply g-string-append body)
              #$tag-name)))

(define* (httpd-simple-wsgi-alias package wsgi-path #:key (aliased-path "/"))
  (let ((wsgi-file (file-append package wsgi-path)))
    (g-string-append
     (httpd-tag 'Files (list wsgi-file)
                (httpd-directive 'Require 'all 'granted))
     (httpd-directive 'WSGIScriptAlias aliased-path wsgi-file))))

(define-record-type* <koszko-httpd-site-conf>
  koszko-httpd-site-conf make-koszko-httpd-site-conf
  koszko-httpd-site-conf?
  (name-and-aliases     koszko-httpd-site-conf-name-and-aliases)
  (body                 koszko-httpd-site-conf-body)
  (auto-www-aliases     koszko-httpd-site-conf-auto-www-aliases
                        (default #t)))

(define* (make-virtualhost-directives site-conf #:key (tls? #f))
  (match-record site-conf <koszko-httpd-site-conf>
    (name-and-aliases body auto-www-aliases)
    (let ((name (car name-and-aliases))
          (aliases (cdr name-and-aliases)))
      `(,(httpd-directive 'ServerName name)
        ,@(map (cut httpd-directive 'ServerAlias <>)
               aliases)
        ,@(if auto-www-aliases
              (map (lambda (alias-or-name)
                     (httpd-directive
                      'ServerAlias (string-append "www." alias-or-name)))
                   name-and-aliases)
              '())
        ,(httpd-directive 'ServerAdmin "koszko@koszko.org")
        ,(httpd-tag 'If (list (format #f "%{HTTP_HOST} != '~a'" name))
                    (httpd-directive 'Redirect 'permanent "/"
                                     (format #f "http~a://~a/"
                                             (if tls? "s" "") name)))
        ,(httpd-tag 'ElseIf
                    (list (format #f "~a && (~a !~~ ~a)"
                                  (if tls? "false" "true")
                                  "%{REQUEST_URI}"
                                  "m#^/[.]well-known/acme-challenge/.*#"))
                    (httpd-directive 'Redirect 'permanent "/"
                                     (format #f "https://~a/" name)))
        ,(httpd-directive
          'Alias "/.well-known/acme-challenge" "/srv/http/acme-challenge/")
        ,@body
        ,@(if tls?
              (let ((cert-base (format #f "/etc/cert-links/guixbot_~a" name)))
                (list (httpd-directive 'SSLEngine 'on)
                      (httpd-directive 'SSLCertificateFile
                                       (string-append cert-base "/cert.pem"))
                      (httpd-directive 'SSLCertificateKeyFile
                                       (string-append cert-base "/privkey.pem"))
                      (httpd-directive 'SSLCertificateChainFile
                                       (string-append cert-base "/chain.pem"))))
              '())))))

(define (make-virtualhosts koszko-site-conf-record)
  (list (httpd-virtualhost
         "*:80"
         (make-virtualhost-directives koszko-site-conf-record))
        (httpd-virtualhost
         "*:443"
         (make-virtualhost-directives koszko-site-conf-record #:tls? #t))))

(define %all-site-confs
  (list))

(define (add-site-conf conf)
  (set! %all-site-confs (append %all-site-confs (list conf))))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases '("koszko.org" "koszkonutek-tmp.pl.eu.org"))
  (body
   `(,(httpd-directive 'DocumentRoot "/srv/http/koszko.org")

     ,(httpd-directive 'Alias "/sideload" "/srv/http/koszko.org")

     ,(httpd-simple-wsgi-alias
       koszko-org-website "/share/koszko-org-website/wsgi.py")))))

(define %cgitrc-text
  (g-string-join
   `("css=/cgit-static/cgit.css"
     "logo=/cgit-static/cgit.png"
     "favicon=/cgit-static/favicon.ico"
     "js="

     ;;,(g-string-append "source-filter="
     ;;                  cgit "/lib/cgit/filters/syntax-highlighting.py")
     "snapshots=tar.gz zip"
     "project-list=/var/lib/gitolite3/projects.list"
     "remove-suffix=1"
     "virtual-root=/"

     "enable-index-links=1"
     "enable-index-owner=0"
     "footer=/var/lib/gitolite3/cgit-footer"
     "max-blob-size=100"
     "root-desc=repositories of Wojtek"

     "mimetype.gif=image/gif"
     "mimetype.html=text/html"
     "mimetype.jpg=image/jpeg"
     "mimetype.jpeg=image/jpeg"
     "mimetype.pdf=application/pdf"
     "mimetype.png=image/png"
     "mimetype.svg=image/svg+xml"

     ,(g-string-append "about-filter="
                       cgit "/lib/cgit/filters/about-formatting.sh")

     ,@(apply append
              (map (lambda (file-name)
                     (map (cut string-append "readme=:" file-name <>)
                          '(".md" ".mkd" ".rst" ".html" ".htm" ".txt" "")))
                   '("readme" "README" "install" "INSTALL")))

     "scan-path=/var/lib/gitolite3/repositories")
   "\n"))

(define %cgitrc-file
  (computed-file "cgitrc"
    #~(with-output-to-file #$output
        (lambda () (display #$%cgitrc-text)))))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases '("git.koszko.org" "git.koszkonutek-tmp.pl.eu.org"
                      "git.happyhacking.pl"))
  (body
   `(;; Hachette got renamed to "Haketilo", repo moved
     ,(httpd-directive
       'Redirect 'permanent "/hachette-fixes-demo" "/haketilo-fixes-demo")
     ;; Old Hydrilla repo now also hosts Haketilo proxy and got renamed to
     ;; "haketilo-hydrilla"
     ,(httpd-directive
       'Redirect 'permanent "/pydrilla" "/haketilo-hydrilla")

     ;; Make HTTP clone happen through git-core instead of through CGit. CGit
     ;; only supports old HTTP "dumb" cloning protocol while we want the new
     ;; "smart" protocol.
     ,(httpd-tag 'Directory (list (file-append git "/libexec/git-core"))
                 (httpd-directive 'Require 'all 'granted)
                 (httpd-directive
                  'SetEnv "GIT_PROJECT_ROOT" "/var/lib/gitolite3/repositories")
                 (httpd-directive
                  'SetEnv "GIT_HTTP_EXPORT_ALL"))

     ,(httpd-directive
       'ScriptAliasMatch
       "^/(.*/(HEAD|info/refs|objects/info/[^/]+|git-upload-pack))$"
       (file-append git "/libexec/git-core/git-http-backend/$1"))

     ;; Once all git-http-backend paths got handled, handle CGit ones.
     ,(httpd-directive
       'Alias "/cgit-static" (file-append cgit "/share/cgit"))

     ,(httpd-directive
       'SetEnv "CGIT_CONFIG" %cgitrc-file)

     ,(httpd-directive
       'ScriptAlias "/" (file-append cgit "/lib/cgit/cgit.cgi/"))

     ,(httpd-tag 'Directory (list (file-append cgit "/lib/cgit/"))
                 (httpd-directive 'Options '+ExecCGI))))))

(for-each
 (lambda (name)
   (add-site-conf
    (koszko-httpd-site-conf
     (name-and-aliases (list (string-append name ".koszko.org")))
     (body
      `(,(httpd-directive
          'DocumentRoot (file-append
                         sheets-websites
                         (string-append "/share/" name "-website"))))))))
 '("sheets" "pray"))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases (list "hydrillabugs.koszko.org" "hachettebugs.koszko.org"))
  (body
   `(,(httpd-directive 'DocumentRoot "/srv/http/hydrillabugs.koszko.org")

     ,(httpd-directive 'RewriteEngine 'On)

     ,(httpd-directive 'RewriteCond "%{REQUEST_METHOD}" "!GET")
     ,(httpd-directive 'RewriteRule "^(.*)$" "/hydrillabugs-archived.html"
                       "[L,R=301,QSD]")

     ,(httpd-directive 'Alias "/hydrillabugs-archived.html"
                       (local-file "./hydrillabugs-archived.html"))

     ,(httpd-directive 'RewriteRule "/uri-map.txt" "/dummy" "[L,F]")

     ;; Serve wget-archived website.
     ,(httpd-directive 'RewriteMap 'add-ext
                       "txt:/srv/http/hydrillabugs.koszko.org/uri-map.txt")

     ,(httpd-tag 'Location (list "/javascripts")
                 (httpd-directive 'ForceType "application/javascript"))

     ,(httpd-directive 'RewriteCond "%{QUERY_STRING}" "!^$")
     ,(httpd-directive 'RewriteCond "${add-ext:$1?%{QUERY_STRING}|NONE}"
                       "!NONE")
     ,(httpd-directive 'RewriteRule "^(.*)$" "${add-ext:$1?%{QUERY_STRING}}"
                       "[L,R=301]")

     ,(httpd-directive 'RewriteCond "%{QUERY_STRING}" "!^$")
     ,(httpd-directive 'RewriteRule "^(.*)$" "$1?%{QUERY_STRING}?" "[L,QSL]")

     ,(httpd-directive 'RewriteCond "%{QUERY_STRING}" "^$")
     ,(httpd-directive 'RewriteCond "${add-ext:$1|NONE}" "!NONE")
     ,(httpd-directive 'RewriteRule "^(.*)$" "${add-ext:$1}" "[L,R=301]")))))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases (list "haketilo.koszko.org"))
  (body
   (list (httpd-simple-wsgi-alias
          hydrilla-website "/share/hydrilla-website/wsgi.py")))))

(define %python-path-spec-sexp
   (search-path-specification->sexp
    (guix-pythonpath-search-path (package-version (default-python)))))

(define %hydrilla-pythonpath-inputs
  (cons hydrilla-without-haketilo
        (map cadr (package-transitive-target-inputs
                   hydrilla-without-haketilo))))

(define %hydrilla-pythonpath-gexp
  (with-imported-modules (source-module-closure '((guix search-paths)))
    #~(begin
        (use-modules (guix search-paths))
        (let ((evaluated-list (evaluate-search-paths
                      (list (sexp->search-path-specification
                             '#$%python-path-spec-sexp))
                      '#$%hydrilla-pythonpath-inputs)))
          (cdar evaluated-list)))))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases (list "hydrilla.koszko.org"))
  (body
   `(,(httpd-directive
       'Alias "/downloads"
       "/srv/http/hydrilla.koszko.org/downloads")

     ,(httpd-directive
       'Alias "/schemas"
       (file-append hydrilla-json-schemas "/share/hydrilla-json-schemas"))

     ,(httpd-directive 'DocumentRoot "/var/lib/hydrilla/malcontent_dirs")

     ,(httpd-tag 'Location '("~" "^/api_v[^/]+/(resource|mapping)/")
                 (httpd-directive 'ForceType 'application/json))

     ,(httpd-directive
       'SetEnvIf 'Request_URI "^/(api_v[0-9]+)/"
       "MALCONENT_DIR=/var/lib/hydrilla/malcontent_dirs/$1")

     ,(httpd-directive
       'SetEnvIf 'Request_URI "^/api_v[0-9]+/"
       (g-string-append "HYDRILLA_GUIX_PYTHONPATH=" %hydrilla-pythonpath-gexp))

     ,(httpd-directive
       'WSGIScriptAliasMatch
       "^/api_v[^/]+/((resource|mapping)/[^/]+[.]json|query|list_all)$"
       (g-string-append (local-file (string-append %here "/hydrilla-wsgi.py"))
                        "/$1"))))))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases (list "hydrillarepos.koszko.org"))
  (auto-www-aliases #f)
  (body
   `(,(httpd-directive 'DocumentRoot "/srv/http/hydrillarepos.koszko.org")

     ,(httpd-directive 'Options '+Indexes)))))

(add-site-conf
 (koszko-httpd-site-conf
  (name-and-aliases (list "test4.koszko.org"))
  (auto-www-aliases #f)
  (body
   `(,(httpd-directive 'DocumentRoot "/srv/http/test")))))

(define %ssl-module
  (httpd-module
   (name "ssl_module")
   (file (file-append httpd "/modules/mod_ssl.so"))))

(define %cgid-module
  (httpd-module
   (name "cgid_module")
   (file (file-append httpd "/modules/mod_cgid.so"))))

(define %wsgi-module
  (httpd-module
   (name "wsgi_module")
   (file (file-append mod-wsgi "/modules/mod_wsgi.so"))))

(define %proxy-http-modules
  (list (httpd-module
         (name "proxy_module")
         (file (file-append httpd "/modules/mod_proxy.so")))
        (httpd-module
         (name "proxy_http_module")
         (file (file-append httpd "/modules/mod_proxy_http.so")))))

(define %rewrite-module
  (httpd-module
   (name "rewrite_module")
   (file (file-append httpd "/modules/mod_rewrite.so"))))

;; logio is needed for the '%O' log format directive
(define %logio-module
  (httpd-module
   (name "logio_module")
   (file (file-append httpd "/modules/mod_logio.so"))))

(define %logformat-combined
  "\"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\"")

(define koszko-httpd-service-type
  (service-type
   (inherit httpd-service-type)
   (extensions (filter
                (lambda (ext)
                  (not (eq? (service-extension-target ext)
                            account-service-type)))
                (service-type-extensions httpd-service-type)))))

(define %koszko-httpd-service
  (service
   koszko-httpd-service-type
   (httpd-configuration
    (config
     (httpd-config-file
      (server-name "koszko.org")
      (listen '("80" "443"))
      (error-log "/var/log/httpd/error.log")
      (modules `(,%ssl-module
                 ,%cgid-module
                 ,%wsgi-module
                 ,@%proxy-http-modules
                 ,%rewrite-module
                 ,%logio-module
                 ,@%default-httpd-modules))
      (extra-config
       (list
        (string-join `("LogFormat" ,%logformat-combined "combined")) "\n"
        "CustomLog /var/log/httpd/access.log combined" "\n"
        "ScriptSock /var/run/cgid.sock" "\n")))))))

(define (extension-of-type? ext type)
  (eq? (service-type-name (service-extension-target ext))
       (service-type-name type)))

(define %all-site-domains
  (map (match-record-lambda <koszko-httpd-site-conf>
         (name-and-aliases auto-www-aliases)
         (let ((www-aliases (map (cut string-append "www." <>)
                                 (if auto-www-aliases name-and-aliases '()))))
           (append name-and-aliases www-aliases)))
       %all-site-confs))

(define %all-site-cert-names
  (map (lambda (domains)
         (format #f "guixbot_~a" (car domains)))
       %all-site-domains))

(define %g-certsaccess-gid
  #~(group:gid (getgrnam "certsaccess")))

(define %setup-ca-gexp
  #~(begin
      (use-modules (guix build utils))

      (define initial-umask
        (umask))

      (mkdir-p "/etc/self-issued/")

      (umask #o027)

      (for-each (lambda (path)
                  (mkdir-p path)
                  (chown path 0 #$%g-certsaccess-gid))
                '("/etc/cert-links" "/etc/self-issued/certs"))

      (umask #o266)

      (unless (file-exists? "/etc/self-issued/ca-key.pem")
        (copy-file #$(local-file "./test-ca-key.pem")
                   "/etc/self-issued/ca-key.pem"))

      (umask #o022)

      (unless (file-exists? "/etc/self-issued/ca-cert.pem")
        (copy-file #$(local-file "./test-ca-cert.pem")
                   "/etc/self-issued/ca-cert.pem"))))

(define g-activate-certs
  (with-imported-modules '((guix build utils))
    #~(lambda (cert-names domain-lists)
        (use-modules (guix build utils)
                     (ice-9 format)
                     (ice-9 textual-ports))

        (define (issue-cert cert-name domains)
          (let* ((cert-dir (format #f "/etc/self-issued/certs/~a" cert-name))
                 (cert-path (format #f "~a/cert.pem" cert-dir))
                 (chain-path (format #f "~a/chain.pem" cert-dir))
                 (fullchain-path (format #f "~a/fullchain.pem" cert-dir))
                 (privkey-path (format #f "~a/privkey.pem" cert-dir))
                 (csr-path (format #f "~a/csr.pem" cert-dir))
                 (openssl (string-append #$openssl "/bin/openssl"))
                 (initial-umask (umask)))
            (unless (false-if-exception (> (stat:gid (stat cert-path)) 0))
              ;; Prepare the directory and private key.
              (umask #o027)
              (mkdir-p cert-dir)
              (system* openssl "genrsa" "-out" privkey-path "2048")

              ;; Make cert.pem and chain.pem.
              (with-output-to-file "/tmp/sth"
                (lambda _
                  (display (format #f "subjectAltName=~{DNS:~a~^,~}"
                                   domains))))
              (system* openssl "req"
                       "-new" "-nodes" "-out" csr-path "-key" privkey-path
                       "-subj" "/CN=My Server/C=PL/ST=PL/L=Krakow/O=Koszko"
                       "-addext" (format #f "subjectAltName=~{DNS:~a~^,~}"
                                         domains))
              (system* openssl "x509"
                       "-req" "-in" csr-path "-copy_extensions=copyall"
                       "-CA" "/etc/self-issued/ca-cert.pem"
                       "-CAkey" "/etc/self-issued/ca-key.pem" "-CAcreateserial"
                       "-out" cert-path "-days" "3650" "-sha256")
              (copy-file "/etc/self-issued/ca-cert.pem" chain-path)

              ;; Concatenate cert.pem and chain.pem into fullchain.pem.
              (with-output-to-file fullchain-path
                (lambda _
                  (for-each (lambda (part)
                              (call-with-input-file part
                                (lambda (port)
                                  (display (get-string-all port)))))
                            (list cert-path chain-path))))

              ;; Correct permissions.
              (chmod privkey-path #o640)
              (for-each (lambda (path)
                          (chown path 0 #$%g-certsaccess-gid))
                        (list cert-dir
                              privkey-path
                              cert-path
                              chain-path
                              fullchain-path))
              (umask initial-umask))))

        (for-each (lambda (cert-name domains)
                    (let* ((link-name (format #f "/etc/cert-links/~a"
                                              cert-name))
                           (tmp-link-name (format #f "~a.newlink" link-name))
                           (letsencrypt-target-name
                            (format #f "/etc/letsencrypt/live/~a" cert-name))
                           (self-issued-target-name
                            (format #f "/etc/self-issued/certs/~a" cert-name)))
                      (when (#$g-symlink-exists? tmp-link-name)
                        (delete-file tmp-link-name))
                      (if (file-exists? letsencrypt-target-name)
                          (symlink letsencrypt-target-name tmp-link-name)
                          (begin
                            (issue-cert cert-name domains)
                            (symlink self-issued-target-name tmp-link-name)))
                      (rename-file tmp-link-name link-name)))
                  cert-names domain-lists))))

(define (make-httpd-deploy-hook cert-name domains)
  (program-file
   "httpd-deploy-hook"
   #~(begin
       (define %cert-name
         #$cert-name)

       (define %domains
         '#$domains)

       (for-each (lambda (subdir)
                   (system* "chgrp" "-R" "certsaccess"
                            (format #f "/etc/letsencrypt/~a/~a"
                                    subdir %cert-name)))
                 '("live" "archive"))

       (system* "find" (format #f "/etc/letsencrypt/archive/~a" %cert-name)
                "-name" "privkey1.pem"
                "-exec" "chmod" "640" "{}" ";")

       (#$g-activate-certs (list %cert-name) (list %domains))

       (let ((pid (call-with-input-file "/var/run/httpd" read)))
         (if pid
             (kill pid SIGHUP)
             #t)))))

(define %certbot-token-filename-gexp
  #~(format #f "/srv/http/acme-challenge/~a" (getenv "CERTBOT_TOKEN")))

(define %koszko-certbot-auth-hook
  (program-file
   "cert-auth-hook"
   (with-imported-modules '((guix build utils))
     #~(begin
         (use-modules (guix build utils))
         (let ((filename #$%certbot-token-filename-gexp))
           (mkdir-p (dirname filename))
           (with-output-to-file filename
             (lambda () (display (getenv "CERTBOT_VALIDATION")))))))))

(define %koszko-certbot-cleanup-hook
  (program-file
   "cert-cleanup-hook"
   #~(delete-file #$%certbot-token-filename-gexp)))

(define (adapt-certbot-shepherd-root-extension ext)
  ;; Make certbot independent of Nginx.
  (let ((old-activation (service-extension-compute ext)))
    (define (new-activation certbot-config)
      (cons
       ;; Dummy Nginx shepherd service will prevent reloading in certbot from
       ;; throwing "&service-not-found-error: nginx".
       (shepherd-service
        (provision '(nginx))
        (one-shot? #t)
        (start #~(const #t))
        (documentation "Provide dummy Nginx.")
        (actions
         (list
          (shepherd-action
           (name 'reload)
           (documentation "Dummy reload")
           (procedure #~(const #t))))))

       (map (lambda (shepherd-service-record)
              (shepherd-service
               (inherit shepherd-service-record)
               (requirement '(httpd))))
            (old-activation certbot-config))))

    (service-extension shepherd-root-service-type new-activation)))

(define koszko-certbot-service-type
  (service-type
   (inherit certbot-service-type)
   (name 'koszko-certbot)
   ;; Prevent certbot from pulling in Nginx — we use Apache here.
   (extensions (filter-map
                (lambda (ext)
                  (cond ((extension-of-type? ext nginx-service-type)
                         #f)
                        ((extension-of-type? ext shepherd-root-service-type)
                         (adapt-certbot-shepherd-root-extension ext))
                        (else
                         ext)))
                (service-type-extensions certbot-service-type)))))

(define %koszko-certificate-configurations
  (map (lambda (cert-name domains)
         (certificate-configuration
          (name cert-name)
          (domains domains)
          (challenge "http")
          (authentication-hook %koszko-certbot-auth-hook)
          (cleanup-hook %koszko-certbot-cleanup-hook)
          (deploy-hook (make-httpd-deploy-hook cert-name domains))))
       %all-site-cert-names %all-site-domains))

(define %koszko-certbot-service
  (service koszko-certbot-service-type
           (certbot-configuration
            (email "koszko@koszko.org")
            (rsa-key-size 4096)
            (certificates %koszko-certificate-configurations))))

(define %koszko-certs-activation-service
  (simple-service 'koszko-certs-activation-service
                  activation-service-type
                  #~(begin
                      #$%setup-ca-gexp
                      (#$g-activate-certs '#$%all-site-cert-names
                                          '#$%all-site-domains))))

(define exim-configuration-config-file
  (@@ (gnu services mail) exim-configuration-config-file))

(define exim-configuration-package
  (@@ (gnu services mail) exim-configuration-package))

(define (adapt-exim-activation-extension ext)
  ;; Make exim logs accessible under /var/log/exim and include current
  ;; configuration from /etc/exim.conf.
  (let ((old-activation (service-extension-compute ext)))
    (define (new-activation exim-config)
      #~(begin
          ;; There's unfortunately no option to tell file-exist? or stat not to
          ;; follow symlinks, hence we use statat...
          (unless (#$g-symlink-exists? "/var/log/exim")
            (symlink "../spool/exim/log" "/var/log/exim"))
          ;; Exim often rereads its config file. Let's substitute it
          ;; atomiacally.
          (with-output-to-file "/etc/exim.conf.new"
            (lambda _
              (format #t "
exim_user = exim
exim_group = exim
exim_path = /run/setuid-programs/exim
.include ~a"
                      #$(exim-configuration-config-file exim-config))))
          (rename-file "/etc/exim.conf.new" "/etc/exim.conf")
          #$(old-activation exim-config)))

    (service-extension activation-service-type new-activation)))

(define (adapt-exim-shepherd-extension ext)
  ;; Make exim daemon use /etc/exim.conf which we made include the real config
  ;; file.
  (let ((old-activation (service-extension-compute ext)))
    (define (new-activation exim-config)
      (let ((exim-package (exim-configuration-package exim-config)))
        (map (lambda (shepherd-service-record)
               (shepherd-service
                (inherit shepherd-service-record)
                (start #~(make-forkexec-constructor
                          '(#$(file-append exim-package "/bin/exim")
                            "-bd" "-v")))))
             (old-activation exim-config))))

    (service-extension shepherd-root-service-type new-activation)))

(define koszko-exim-service-type
  (service-type
   (inherit exim-service-type)
   (extensions (filter-map
                (lambda (ext)
                  (cond
                   ((extension-of-type? ext account-service-type)
                    ;; Avoid double declaration of "exim" user and group.
                    #f)
                   ((extension-of-type? ext activation-service-type)
                    (adapt-exim-activation-extension ext))
                   ((extension-of-type? ext shepherd-root-service-type)
                    (adapt-exim-shepherd-extension ext))
                   (else
                    ext)))
                (service-type-extensions exim-service-type)))))

(define koszko-adapted-exim
  (package/inherit exim
    (arguments
     (substitute-keyword-arguments
         (package-arguments exim)
       ((#:phases phases)
        #~(modify-phases #$phases
            (add-after 'configure 'configure*
              (lambda _
                (substitute* "Local/Makefile"
                  (("# (SUPPORT_MAILDIR=yes)" all line)
                   line)
                  (("(EXIM_USER=).*" all var)
                   (string-append var "106\n"))
                  (("# (EXIM_GROUP=).*" all var)
                   (string-append var "113\n")))))
            (add-after 'install 'symlink-config-file
              (lambda _
                (let ((config-path (string-append #$output "/etc/exim.conf")))
                  (delete-file config-path)
                  (symlink "/etc/exim.conf" config-path))))))))))

(define %koszko-exim-service
  (service koszko-exim-service-type
           (exim-configuration
            (package koszko-adapted-exim)
            (config-file (local-file "./exim.conf")))))

(define %koszko-mail-aliases-service
  (service mail-aliases-service-type
           '(("mailer-daemon" "postmaster")
             ("postmaster" "root")
             ("nobody" "root")
             ("hostmaster" "root")
             ("usenet" "root")
             ("news" "root")
             ("webmaster" "root")
             ("www" "root")
             ("ftp" "root")
             ("abuse" "root")
             ("noc" "root")
             ("security" "root")
             ("root" "urz")
             ("dmarc" "urz")
             ("admin" "urz")
             ("wk" "urz")
             ("koszko" "urz")
             ("my-contribution-is-licensed-cc0" "urz"))))

(define koszko-dovecot-service-type
  (service-type
   (inherit dovecot-service-type)
   (extensions (filter-map
                (lambda (ext)
                  (cond
                   ((extension-of-type? ext account-service-type)
                    ;; Avoid double declaration of "dovecot" and "dovenull"
                    ;; users and groups.
                    #f)
                   (else
                    ext)))
                (service-type-extensions dovecot-service-type)))))

(define %koszko-dovecot-service
  (service koszko-dovecot-service-type
           (dovecot-configuration
            (services (list (service-configuration
                             (kind "imap-login")
                             (listeners (list (inet-listener-configuration
                                               (protocol "imaps")
                                               (port 993)
                                               (ssl? #t))))
                             ;; '1' is more secure than '0' because each
                             ;; connection is handled in a separate process.
                             (service-count 1))
                            (service-configuration
                             (kind "imap"))
                            (service-configuration
                             (kind "auth")
                             (listeners (list (unix-listener-configuration
                                               (path "auth-userdb"))))
                             ;; Dovecot requires process-limit to be 1 here.
                             (service-count 0)
                             (process-limit 1))))
            (ssl-cert "</etc/cert-links/guixbot_koszko.org/fullchain.pem")
            (ssl-key "</etc/cert-links/guixbot_koszko.org/privkey.pem")
            (auth-mechanisms '("plain" "login"))
            (passdbs (list (passdb-configuration
                            (driver "passwd-file")
                            (args '("scheme=SHA256-CRYPT"
                                    "username_format=%u"
                                    "/etc/dovecot/users")))))
            (userdbs (list (userdb-configuration
                            (driver "passwd-file")
                            (args '("username_format=%u"
                                    "/etc/dovecot/users")))))
            (mail-location "maildir:~/Maildir"))))

(define (make-zone-entries domain)
  (define-zone-entries entries
    ;; nameservers
    ("@" "" "IN" "NS" "vps-93-95-227-159.1984.is.")
    ("@" "" "IN" "NS" "ns0.1984.is.")
    ("@" "" "IN" "NS" "ns1.1984.is.")
    ("@" "" "IN" "NS" "ns2.1984.is.")
    ("@" "" "IN" "NS" "ns1.1984hosting.com.")
    ("@" "" "IN" "NS" "ns2.1984hosting.com.")
    ;; domain->IP assignments
    ("*.ctftilde" "" "IN" "CNAME" "ctftilde")
    ("ctftilde" "" "IN" "A" "127.0.0.1")
    ("ctftilde" "" "IN" "AAAA" "::1")

    ("antioch" "" "IN" "A" "188.68.236.76")
    ("antioch" "" "IN" "AAAA" "fe80::216:3eff:feb0:4e18")

    ("salamina" "" "IN" "A" "188.68.237.248")
    ("salamina" "" "IN" "AAAA" "fe80::216:3eff:fee6:bf39")

    ("hachettebugs" "" "IN" "CNAME" "antioch")
    ("www.hachettebugs" "" "IN" "CNAME" "antioch")
    ("hydrillabugs" "" "IN" "CNAME" "antioch")
    ("www.hydrillabugs" "" "IN" "CNAME" "antioch")
    ("imap" "" "IN" "CNAME" "antioch")
    ("pray" "" "IN" "CNAME" "antioch")
    ("www.pray" "" "IN" "CNAME" "antioch")
    ("sheets" "" "IN" "CNAME" "antioch")
    ("www.sheets" "" "IN" "CNAME" "antioch")
    ("smtp" "" "IN" "CNAME" "antioch")

    ("old" "" "IN" "A"     "93.95.227.159")
    ("old" "" "IN" "AAAA"  "fe80::5054:5dff:fe5f:e39f")

    ("@"    "" "IN" "A" "188.68.236.76")
    ("@"    "" "IN" "AAAA" "fe80::216:3eff:feb0:4e18")
    ("*"    "" "IN" "CNAME" "old")
    ;; mail
    ("@"      "" "IN" "MX 10" "smtp.koszko.org.")
    ;; dmarc
    ("@"      "" "IN" "TXT"   "\"v=spf1 ip4:93.95.227.159 ip6:fe80::5054:5dff:fe5f:e39f ip4:188.68.236.76 ip6:fe80::216:3eff:feb0:4e18 -all\"")
    ("_dmarc" "" "IN" "TXT"   "\"v=DMARC1;p=reject;rua=mailto:dmarc@koszko.org;ruf=mailto:dmarc@koszko.org;rf=afrf;pct=100\"")

    ("mail._domainkey" "" "IN" "TXT" "(
     \"k=rsa;t=s;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx0kXNRIL\"
     \"VRDaU1iPdUu2FwX+pRbNS4DwojiOYznESt1npY8LzYV3MBKf2XXOSl+6Ui8Jy91V\"
     \"KzoUqwN9Rh6vdsLYPaMMtPUe/gN1AOqyF4qYqz499VZqRLmoPyq4EV5eRSqbDeDb\"
     \"eDOaoJ0+ZJHG6qg2eAz1v2U++lsSRTOkXe3xZFxrHRrvXg5JVl5DNGRKBjotwW8O\"
     \"EMhwUa2LbmJA/EbbCWhXfmaIEwqP2LRUF2HqFMSr4IHHopcTKQwpSwbsOGYG8MV1\"
     \"c5HelO+OROpuUNPE8YoKVHKwfWdwgStrrkSYK+H5JQvJgFvyfsyePfXfqszde+4B\"
     \"EC34ScPW86HKmw4JltFpBCiBThYdD0fu8g5mQzdtwNUbCcPkuUDrUTA4TE44ScHO\"
     \"VYDX0QWaUubrsf5F1+bwyTKuzbUHXnbXw7r7JLC2P4CjtsS4MLYjrfeQ3TIEdj+s\"
     \"WtWVItIVQnFRuSTFmHKqnWNDSjmTeH5m8FWPQeDjXRj2e1f5vCrfIvyXTzWvOeIw\"
     \"DU2QfyUPUKaL9hvNvX9S3G45qM/CH5UTRc2BC0dFZHBNR/uLTGMYaatfw2QAxQzs\"
     \"cmw34IgwLGswxFj3iaDwc8d3Uh+JamFBf+GUrwjRs/sVRRiXrB+qKwlxckzWHVbV\"
     \"oABCxjKDmvE86L3kCQ+MobG0BOtFBR4BqU8CAwEAAQ==\"
     )")

    ("mail-antioch._domainkey" "" "IN" "TXT" "(
     \"k=rsa;t=s;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsC9HfcId\"
     \"QGcBzYtBzv/VF7vPrRAASwfeAJ3pr0tddpYtFMGGm8kWPAKykX2LvP4epQPdzGL9\"
     \"W9qLJk3R+iJSlUYLgP1s4Tr5qiUm/PEHPnjzwC8kqtJ/CrQs7WLfiuE3SElWI3iQ\"
     \"FMRCD1Kk07axSOCgLbshthEAxZQqScYiZT7Irl4SHm9SpTUJkqh5xcCTqsW1yuG/\"
     \"YWbfERH4PFp2pA85ZZvVEHqd+e8LMLh9EKMYRbhp9AVKMaESIegkO0RZv0KGp4aU\"
     \"vzquG1FYQ9VOJ6KiviZANuEL4cpNrqmFb4Wbuwc6w7drWaHjFT/j1mS/eB0DN3el\"
     \"nPQnWMTJZPhqnFZKzG37P5VkIDjbcotevsBGV28gx3VJNf1ye1x/P9f4fJw2s7EY\"
     \"izNZx9/xfmAiOpP3KhrpE+cccBWnMHYyp0X7ClgBIQTDiLOBX3KhH+GOuADMcum1\"
     \"4y8SHw2xSYGGk46YI0fBf8kaJsAA9wBQvuBFSj1MjxjdWt0GF0OLnGB1RsPZx+nU\"
     \"Q+EZz0PDuv16ofN7MaOfY+kD6XoBCDjj4FDqLOgBgoLS7LfLyYTElbuItj12Yqph\"
     \"ZPY4I+Lmn9nzl3tEnUqtRNu5LPhIR9mj/4BLp2UPgkVnqTAcA+qxoJghm8hh9AS9\"
     \"2pelav0CarwwvUo09rcvujksuwy8TulK8ckCAwEAAQ==\"
     )")

    ((string-append domain "._report._dmarc") "" "IN" "TXT" "\"v=DMARC1\""))

  entries)

(define %koszko-org-zone-configuration
  (knot-zone-configuration
   (domain "koszko.org")
   (zone (zone-file
          (origin "koszko.org")
          (entries (make-zone-entries "koszko.org"))
          (ns "vps-93-95-227-159.1984.is.")
          (mail "koszko")
          (serial 2024042903)))
   (acl '("allow-axfr-from-1984"))
   (semantic-checks? #t)
   (notify '("1984-axfr-remote"))))

(define %koszkonutek-tmp.pl.eu.org-zone-configuration
  (knot-zone-configuration
   (domain "koszkonutek-tmp.pl.eu.org")
   (zone (zone-file
          (origin "koszkonutek-tmp.pl.eu.org")
          (entries (make-zone-entries "koszkonutek-tmp.pl.eu.org"))
          (ns "vps-93-95-227-159.1984.is.")
          (mail "wk")
          (serial 2024042903)))
   (acl '("allow-axfr-from-1984"))
   (semantic-checks? #t)
   (notify '("1984-axfr-remote"))))

(define knot-configuration-knot
  (@@ (gnu services dns) knot-configuration-knot))

(define knot-config-file
  (@@ (gnu services dns) knot-config-file))

(define (knot-config-file-with-logging knot-config)
  (computed-file "knot-with-logging.conf"
    #~(with-output-to-file #$output
        (lambda _
          (use-modules (ice-9 textual-ports))
          (call-with-input-file #$(knot-config-file knot-config)
            (lambda (port)
              (display (get-string-all port))))
          (newline)
          (display "\
log:
  - target: syslog
")))))

(define (adapt-knot-shepherd-extension ext)
  ;; Make knot daemon log to syslog.
  (let ((old-activation (service-extension-compute ext)))
    (define (new-activation knot-config)
      (let* ((knot-package (knot-configuration-knot knot-config))
             (config-file (knot-config-file-with-logging knot-config)))
        (map (lambda (shepherd-service-record)
               (shepherd-service
                (inherit shepherd-service-record)
                (start #~(make-forkexec-constructor
                          '(#$(file-append knot-package "/sbin/knotd")
                            "-c" #$config-file)))))
             (old-activation knot-config))))

    (service-extension shepherd-root-service-type new-activation)))

(define koszko-knot-service-type
  (service-type
   (inherit knot-service-type)
   (extensions (map (lambda (ext)
                      (if (extension-of-type? ext shepherd-root-service-type)
                          (adapt-knot-shepherd-extension ext)
                          ext))
                    (service-type-extensions knot-service-type)))))

(define %koszko-knot-service
  (service koszko-knot-service-type
           (knot-configuration
            (acls (list (knot-acl-configuration
                         (id "allow-axfr-from-1984")
                         (address '("93.95.224.6"))
                         (action '(transfer)))))
            (remotes (list (knot-remote-configuration
                            (id "1984-axfr-remote")
                            (address '("93.95.224.6")))))
            (zones (list %koszko-org-zone-configuration
                         %koszkonutek-tmp.pl.eu.org-zone-configuration)))))

(operating-system
  (host-name "koszko")
  (timezone "Europe/Warsaw")
  (groups (cons*
           ;; Some groups must have explicit ids so that the host can provide
           ;; files that are readable by certain daemons and not readable by the
           ;; world.
           (user-group
            (name "exim")
            (id 113)
            (system? #t))
           (user-group
            (name "dovecot")
            (id 115)
            (system? #t))
           (user-group
            (name "dovenull")
            (id 116)
            (system? #t))
           (user-group
            (name "httpd")
            (id 133)
            (system? #t))
           (user-group
            (name "gitolite3")
            (id 118)
            (system? #t))
           (user-group
            (name "certsaccess")
            (id 1001)
            (system? #t))
           (user-group
            (name "urz")
            (id 1000))
           (user-group
            (name "joanna")
            (id 1003))
           %base-groups))
  (users (cons*
          (user-account
           (name "exim")
           (group "exim")
           (supplementary-groups '("certsaccess"))
           (uid 106)
           (system? #t)
           (comment "Exim daemon user")
           (home-directory "/var/empty")
           (shell (file-append shadow "/sbin/nologin")))
          (user-account
           (name "dovecot")
           (group "dovecot")
           (supplementary-groups '("certsaccess"))
           (uid 108)
           (system? #t)
           (comment "Dovecot daemon user")
           (home-directory "/var/empty")
           (shell (file-append shadow "/sbin/nologin")))
          (user-account
           (name "dovenull")
           (group "dovenull")
           (uid 109)
           (system? #t)
           (comment "Dovecot daemon login user")
           (home-directory "/var/empty")
           (shell (file-append shadow "/sbin/nologin")))
          (user-account
           (name "httpd")
           (group "httpd")
           (supplementary-groups '("gitolite3" "certsaccess"))
           (system? #t)
           (comment "Apache daemon user")
           (home-directory "/var/empty")
           (shell (file-append shadow "/sbin/nologin")))
          ;; The gitolite user must also have an id that matches the respective
          ;; host user's one — otherwise the cgit CGI process floods logs with
          ;; an error about being unable to determine permissions of some
          ;; files...
          (user-account
           (name "gitolite3")
           (group "gitolite3")
           (uid 110)
           (system? #t)
           (comment "Gitolite3")
           (home-directory "/var/empty")
           (shell (file-append shadow "/sbin/nologin")))
          (user-account
           (name "urz")
           (group "urz")
           (supplementary-groups '("cdrom" "floppy" "audio" "video" "netdev"))
           (uid 1000))
          (user-account
           (name "joanna")
           (group "joanna")
           (uid 1001))
          %base-user-accounts))
  (file-systems (cons (file-system
                        (device (file-system-label "does-not-matter"))
                        (mount-point "/")
                        (type "ext4"))
                      %base-file-systems))
  (bootloader (bootloader-configuration
               (bootloader grub-bootloader)
               (targets '("/dev/sdDOES-NOT-MATTER"))))
  (setuid-programs
   (cons* (setuid-program
           (program (file-append koszko-adapted-exim "/bin/exim")))
          %setuid-programs))
  (packages (append (specifications->packages
                     '("nss-certs"
                       "file"
                       "net-tools"
                       "man-pages-posix"))
                    %base-packages))
  (services
   (cons* %koszko-httpd-service
          (simple-service 'koszko-org-website koszko-httpd-service-type
                          (append-map make-virtualhosts %all-site-confs))
          (service
           (shepherd-service-type
            'dummy-network
            (const
             (shepherd-service
              (documentation "Provide 'networking' without actually doing anything")
              (provision '(networking))
              (start #~(const #t))
              (stop #~(const #t))
              (respawn? #f)))
            (description "Make other services assume network is there."))
           #f)
          %koszko-certbot-service
          %koszko-certs-activation-service
          %koszko-exim-service
          %koszko-mail-aliases-service
          %koszko-dovecot-service
          %koszko-knot-service
          %base-services)))