diff --git a/.gitignore b/.gitignore index 2e1278f..9942df7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ *.key config.toml +result +result-* diff --git a/blahctl/src/main.rs b/blahctl/src/main.rs index 4b72678..5c1cf1c 100644 --- a/blahctl/src/main.rs +++ b/blahctl/src/main.rs @@ -20,7 +20,9 @@ use tokio::runtime::Runtime; /// NB. Sync with docs of [`User::url`]. const KEY_URL_SUBPATH: &str = "/.well-known/blah/key"; +/// Control or manage Blah Chat Server. #[derive(Debug, clap::Parser)] +#[clap(about, version = option_env!("CFG_RELEASE").unwrap_or(env!("CARGO_PKG_VERSION")))] struct Cli { #[command(subcommand)] command: Command, diff --git a/blahd/blahd.example.service b/blahd/blahd.example.service new file mode 100644 index 0000000..0402465 --- /dev/null +++ b/blahd/blahd.example.service @@ -0,0 +1,46 @@ +[Unit] +Description=Blah Chat Server +After=network.target + +[Service] +Type=notify +ExecStart=/usr/bin/blahd serve --config ${CONFIGURATION_DIRECTORY}/blahd.toml +ConfigurationDirectory=blahd +StateDirectory=blahd +Restart=always +RestartSec=10s + +# Permission and capabilities + +DynamicUser=yes +AmbientCapabilities=CAP_NET_BIND_SERVICE +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +# 0640 / 0750 +UMask=0027 + +# Sandboxing +# Mostly copied from: https://github.com/NixOS/nixpkgs/blob/6414ef7ca3bf18ec4f9628d09ccc1eb030276ee2/nixos/modules/services/web-servers/nginx/default.nix#L1246 + +LockPersonality=yes +MemoryDenyWriteExecute=yes +NoNewPrivileges=yes +PrivateDevices=yes +PrivateMounts=yes +PrivateUsers=yes +ProcSubset=pid +ProtectClock=yes +ProtectControlGroups=yes +ProtectHome=yes +ProtectHostname=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +ProtectProc=invisible +ProtectProc=invisible +RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 +RestrictNamespaces=yes +RestrictRealtime=yes +SystemCallArchitectures=native +SystemCallFilter=@system-service +SystemCallFilter=~@privileged diff --git a/blahd/config.example.toml b/blahd/config.example.toml index c5b5ca3..80a9855 100644 --- a/blahd/config.example.toml +++ b/blahd/config.example.toml @@ -1,8 +1,12 @@ +# The example configuration file, required options are documented as +# `(Required)`, other options are optional and the example value given here is +# the default value. + [database] -# (Required) # The path to the main SQLite database. -# It will be created and initialized if not exist. -path = "/path/to/db.sqlite" +# The file will be created and initialized if not exist, but missing directory +# will not. +path = "/var/lib/blahd/db.sqlite" [server] diff --git a/blahd/src/config.rs b/blahd/src/config.rs index b8b672b..8b05e42 100644 --- a/blahd/src/config.rs +++ b/blahd/src/config.rs @@ -11,9 +11,11 @@ pub struct Config { pub server: ServerConfig, } +#[serde_inline_default] #[derive(Debug, Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct DatabaseConfig { + #[serde_inline_default("/var/lib/blahd/db.sqlite".into())] pub path: PathBuf, } diff --git a/blahd/src/main.rs b/blahd/src/main.rs index de94cb5..ce21b94 100644 --- a/blahd/src/main.rs +++ b/blahd/src/main.rs @@ -31,7 +31,9 @@ mod middleware; mod config; mod utils; +/// Blah Chat Server #[derive(Debug, clap::Parser)] +#[clap(about, version = option_env!("CFG_RELEASE").unwrap_or(env!("CARGO_PKG_VERSION")))] enum Cli { /// Run the server with given configuration. Serve { diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..51c9ceb --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "naersk": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1721727458, + "narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=", + "owner": "nix-community", + "repo": "naersk", + "rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "naersk", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1724819573, + "narHash": "sha256-GnR7/ibgIH1vhoy8cYdmXE6iyZqKqFxQSVkFgosBh6w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "71e91c409d1e654808b2621f28a327acfdad8dc2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "naersk": "naersk", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..3c27e09 --- /dev/null +++ b/flake.nix @@ -0,0 +1,80 @@ +rec { + description = "Blah Chat Server"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + naersk = { + url = "github:nix-community/naersk"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + naersk, + }: + let + inherit (nixpkgs) lib; + eachSystem = lib.genAttrs lib.systems.flakeExposed; + + rev = self.rev or (lib.warn "Git changes are not committed" (self.dirtyRev or "dirty")); + in + { + packages = eachSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + naersk' = pkgs.callPackage naersk { }; + in + rec { + default = blahd; + blahd = pkgs.callPackage ( + { + pkg-config, + openssl, + sqlite, + }: + naersk'.buildPackage rec { + pname = "blahd"; + src = ./.; + version = "git-${rev}"; + CFG_RELEASE = version; + + nativeBuildInputs = [ + pkg-config + ]; + buildInputs = [ + openssl + sqlite + ]; + + cargoBuildOptions = opts: opts ++ [ + "--package=blahd" + "--package=blahctl" + ]; + + postInstall = '' + mkdir -p $out/etc/systemd/system + substitute ./blahd/blahd.example.service $out/etc/systemd/system/blahd.service \ + --replace-fail '/usr/bin/blahd' "$out/bin/blahd" + ''; + + meta = { + inherit description; + homepage = "https://github.com/Blah-IM/blahrs"; + }; + } + ) { }; + } + ); + + nixosModules = rec { + default = blahd; + blahd = import ./nix/module.nix { + inherit self; + }; + }; + }; +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..13004a4 --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,98 @@ +{ self }: +{ + lib, + config, + pkgs, + ... +}: +let + inherit (lib) + literalMD + mdDoc + mkEnableOption + mkIf + mkOption + types + ; + + cfg = config.services.blahd; + + toml = pkgs.formats.toml { }; + mkConfigFile = + name: config: + (toml.generate name config).overrideAttrs (old: { + buildCommand = + old.buildCommand + + '' + ${lib.getBin cfg.package}/bin/blahd validate --config $out + ''; + }); + + settingsType = types.submodule { + freeformType = toml.type; + + # TODO: Auto-generate these options? Now only required options are documented. + options = { + database.path = mkOption { + type = types.path; + default = "/var/lib/blahd/db.sqlite"; + }; + + server.listen = mkOption { + type = types.str; + example = "localhost:8080"; + }; + + server.base_url = mkOption { + type = types.str; + example = "http://localhost:8080"; + }; + }; + }; + +in +{ + options.services.blahd = { + enable = mkEnableOption "Blah Chat Server"; + + package = mkOption { + description = mdDoc "The blahd package to use."; + type = types.package; + default = self.packages.${pkgs.system}.blahd; + defaultText = literalMD "blahd package from its flake output"; + }; + + settings = mkOption { + description = '' + blahd configuration. + Will be ignored if `settingsFile` is non-null. + ''; + type = settingsType; + }; + + settingsFile = mkOption { + description = '' + blahd configuration file path. + If non-null, this will be used and `settings` will be ignored. + ''; + type = types.nullOr types.path; + defaultText = literalMD "generated from `settings`"; + default = mkConfigFile "blahd.toml" cfg.settings; + }; + }; + + config = mkIf cfg.enable { + systemd.packages = [ cfg.package ]; + environment.systemPackages = [ cfg.package ]; + + systemd.services."blahd" = { + overrideStrategy = "asDropin"; + + wantedBy = [ "multi-user.target" ]; + restartIfChanged = false; + stopIfChanged = false; + }; + + environment.etc."blahd/blahd.toml".source = cfg.settingsFile; + }; +}