commit 4670b8546e247630b833f52bff11ab2b39daa2d7 Author: niten Date: Wed May 29 17:54:05 2024 -0700 Create cl-gemini flake diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a92d0d2 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "Common Lisp Gemini server."; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-22.05"; + utils.url = "github:numtide/flake-utils"; + lisp-repo.url = "git+https://fudo.dev/public/lisp-repository.git"; + cl-gemini = { + url = "git+https://fudo.dev/informis/cl-gemini.git"; + flake = false; + }; + }; + + outputs = { nixpkgs, lisp-repo, cl-gemini, utils, ... }: + utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + lispPackages = pkgs.lispPackages // lisp-repo.packages."${system}"; + in { + packages = rec { + cl-gemini = pkgs.callPackage ./package.nix { inherit lispPackages; }; + cl-gemini-launcher = + pkgs.callPackage ./launcher.nix { inherit lispPackages cl-gemini; }; + default = cl-gemini; + }; + }) // { + nixosModules = rec { + cl-gemini = import ./module.nix; + default = cl-gemini; + }; + }; +} diff --git a/launcher.nix b/launcher.nix new file mode 100644 index 0000000..1c358ff --- /dev/null +++ b/launcher.nix @@ -0,0 +1,47 @@ +{ lib, lispPackages, openssl_1_1, sbcl, writeShellApplication, cl-gemini, ... }: + +with lib; +let + serverLauncher = pkgs.writeText "launch-cl-gemini.lisp" '' + (defun getenv-or-fail (env-var &optional default) + (let ((value (uiop:getenv env-var))) + (if (null value) + (if default + default + (uiop:die 1 "unable to find required env var: ~A" env-var)) + value))) + + (require :asdf) + (asdf:load-system :slynk) + (asdf:load-system :cl-gemini) + (let ((slynk-port (uiop:getenvp "GEMINI_SLYNK_PORT"))) + (when slynk-port + (slynk:create-server :port (parse-integer slynk-port) :dont-close t))) + (let ((feed-file (uiop:getenvp "GEMINI_FEEDS"))) + (when feed-file + (load feed-file))) + (cl-gemini:start-gemini-server + (getenv-or-fail "GEMINI_LISTEN_IP") + (getenv-or-fail "GEMINI_PRIVATE_KEY") + (getenv-or-fail "GEMINI_CERTIFICATE") + :port (parse-integer (getenv-or-fail "GEMINI_LISTEN_PORT")) + :document-root (getenv-or-fail "GEMINI_DOCUMENT_ROOT") + :textfiles-root (getenv-or-fail "GEMINI_TEXTFILES_ROOT") + :log-stream *standard-output* + :threaded t + :separate-thread t) + (loop (sleep 10)) + ''; + + sbcl-with-ssl = sbcl.overrideAttrs (oldAttrs: rec { + propagatedBuildInputs = oldAttrs.buildInputs ++ [ openssl_1_1.dev ]; + }); + +in writeShellApplication { + name = "cl-gemini-launcher"; + + runtimeInputs = [ asdf sbcl-with-ssl openssl_1_1 cl-gemini ]; + + text = + "${lispPackages.clwrapper}/bin/common-lisp.sh --load ${server-launcher}"; +} diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..f1120b8 --- /dev/null +++ b/module.nix @@ -0,0 +1,179 @@ +{ lisp-repo }: + +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.informis.cl-gemini; + + inherit (lisp-repo.lib) lispSourceRegistry; + + feedOpts = { ... }: { + options = with types; { + url = mkOption { + type = str; + description = + "Base URL of the feed, ie. the URL corresponding to the feed path."; + example = "gemini://my.server/path/to/feed-files"; + }; + + title = mkOption { + type = str; + description = "Title of given feed."; + example = "My Fancy Feed"; + }; + + path = mkOption { + type = str; + description = "Path to Gemini files making up the feed."; + example = "/path/to/feed"; + }; + }; + }; + + generate-feeds = feeds: + let + feed-strings = mapAttrsToList (feed-name: opts: + '' + (cl-gemini:register-feed :name "${feed-name}" :title "${opts.title}" :path "${opts.path}" :base-uri "${opts.url}")'') + feeds; + in pkgs.writeText "gemini-local-feeds.lisp" + (concatStringsSep "\n" feed-strings); + +in { + options.services.cl-gemini = with types; { + enable = mkEnableOption "Enable the cl-gemini server."; + + port = mkOption { + type = port; + description = "Port on which to serve Gemini traffic."; + default = 1965; + }; + + hostname = mkOption { + type = str; + description = + "Hostname at which the server is available (for generating the SSL certificate)."; + example = "my.hostname.com"; + }; + + user = mkOption { + type = str; + description = "User as which to run the cl-gemini server."; + default = "cl-gemini"; + }; + + server-ip = mkOption { + type = str; + description = "IP on which to serve Gemini traffic."; + example = "1.2.3.4"; + }; + + document-root = mkOption { + type = str; + description = "Root at which to look for gemini files."; + example = "/my/gemini/root"; + }; + + user-public = mkOption { + type = str; + description = "Subdirectory of user homes to check for gemini files."; + default = "gemini-public"; + }; + + slynk-port = mkOption { + type = nullOr port; + description = "Port on which to open a slynk server, if any."; + default = null; + }; + + feeds = mkOption { + type = attrsOf (submodule feedOpts); + description = + "Feeds to generate and make available (as eg. /feed/name.xml)."; + example = { + diary = { + title = "My Diary"; + path = "/path/to/my/gemfiles/"; + url = "gemini://my.host/blog-path/"; + }; + }; + default = { }; + }; + + textfiles-archive = mkOption { + type = str; + description = "A path containing only gemini & text files."; + example = "/path/to/textfiles/"; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ cfg.port ]; + + systemd.services.cl-gemini = { + description = + "cl-gemini Gemini server (https://gemini.curcumlunar.space/)."; + + path = [ cl-gemini-launcher ]; + + serviceConfig = let + genKeyCommand = { hostname, key, certs, ... }: + concatStringsSep " " [ + "${pkgs.openssl_1_1}/bin/openssl req -new" + ''-subj "/CN=.${hostname}"'' + ''-addext "subjectAltName = DNS:${hostname}, DNS:.${hostname}"'' + "-x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1" + "-days 3650" + "-nodes" + "-out ${cert}" + "-keyout ${key}" + ]; + + genKey = { key, cert, ... }@opts: + pkgs.writeShellScript "cl-gemini-generate-key.sh" '' + if [[ ! -f ${key} ]]; then + ${genKeyCommand opts} + chown 0400 ${key} + chown 0400 ${cert} + else + echo "ssl key exists, skipping generation" + fi + ''; + + in { + ExecStart = "cl-gemini-launcher"; + ExecStartPre = genKey { + inherit (cfg) hostname; + key = "$RUNTIME_DIRECTORY/key.pem"; + cert = "$RUNTIME_DIRECTORY/cert.pem"; + }; + Restart = "on-failure"; + DynamicUser = true; + RuntimeDirectory = "cl-gemini"; + LoadCredential = [ + "key.pem:${cfg.ssl-private-key}" + "cert.pem:${cfg.ssl-certificate}" + ]; + }; + + environment = { + GEMINI_SLYNK_PART = + mkIf (cfg.slynk-port != null) (toString cfg.slynk-port); + GEMINI_LISTEN_IP = cfg.server-ip; + GEMINI_PRIVATE_KEY = "$RUNTIME_DIRECTORY/key.pem"; + GEMINI_CERTIFICATE = "$RUNTIME_DIRECTORY/cert.pem"; + GEMINI_LISTEN_PORT = toString cfg.port; + GEMINI_DOCUMENT_ROOT = cfg.document-root; + GEMINI_TEXTFILES_ROOT = textfiles-archive; + GEMINI_FEEDS = "${generate-feeds cfg.feeds}"; + + CL_SOURCE_REGISTRY = lispSourceRegistry cl-gemini; + }; + + path = [ gcc file getent ]; + + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/package.nix b/package.nix new file mode 100644 index 0000000..3f4b801 --- /dev/null +++ b/package.nix @@ -0,0 +1,35 @@ +{ inputs, lispPackages, buildLispPackage, ... }: + +buildLispPackage { + baseName = "cl-gemini"; + packageName = "cl-gemini"; + description = "Gemini server written in Common Lisp."; + + buildSystems = [ "cl-gemini" ]; + + src = inputs.cl-gemini; + + deps = with lispPackages; [ + alexandria + arrows + asdf-package-system + asdf-system-connections + cl_plus_ssl + cl-ppcre + fare-mop + file-types + inferior-shell + local-time + osicat + quicklisp + quri + slynk + slynk-macrostep + slynk-stepper + uiop + usocket-server + xml-emitter + ]; + + asdFilesToKeep = [ "cl-gemini.asd" ]; +}