diff options
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/doc/manual/release-notes/rl-2405.section.md | 2 | ||||
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/misc/anki-sync-server.md | 68 | ||||
-rw-r--r-- | nixos/modules/services/misc/anki-sync-server.nix | 140 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/anki-sync-server.nix | 71 |
6 files changed, 283 insertions, 0 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md index f25999f607d9e..749ebc5cb13b6 100644 --- a/nixos/doc/manual/release-notes/rl-2405.section.md +++ b/nixos/doc/manual/release-notes/rl-2405.section.md @@ -16,6 +16,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable). +- [Anki Sync Server](https://docs.ankiweb.net/sync-server.html), the official sync server built into recent versions of Anki. Available as [services.anki-sync-server](#opt-services.anki-sync-server.enable). + ## Backward Incompatibilities {#sec-release-24.05-incompatibilities} <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. --> diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 4dd7563aa27e9..7f708f6e57c70 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -637,6 +637,7 @@ ./services/misc/amazon-ssm-agent.nix ./services/misc/ananicy.nix ./services/misc/ankisyncd.nix + ./services/misc/anki-sync-server.nix ./services/misc/apache-kafka.nix ./services/misc/atuin.nix ./services/misc/autofs.nix diff --git a/nixos/modules/services/misc/anki-sync-server.md b/nixos/modules/services/misc/anki-sync-server.md new file mode 100644 index 0000000000000..5d2b4da4d2fc2 --- /dev/null +++ b/nixos/modules/services/misc/anki-sync-server.md @@ -0,0 +1,68 @@ +# Anki Sync Server {#module-services-anki-sync-server} + +[Anki Sync Server](https://docs.ankiweb.net/sync-server.html) is the built-in +sync server, present in recent versions of Anki. Advanced users who cannot or +do not wish to use AnkiWeb can use this sync server instead of AnkiWeb. + +This module is compatible only with Anki versions >=2.1.66, due to [recent +enhancements to the Nix anki +package](https://github.com/NixOS/nixpkgs/commit/05727304f8815825565c944d012f20a9a096838a). + +## Basic Usage {#module-services-anki-sync-server-basic-usage} + +By default, the module creates a +[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/) +unit which runs the sync server with an isolated user using the systemd +`DynamicUser` option. + +This can be done by enabling the `anki-sync-server` service: +``` +{ ... }: + +{ + services.anki-sync-server.enable = true; +} +``` + +It is necessary to set at least one username-password pair under +{option}`services.anki-sync-server.users`. For example + +``` +{ + services.anki-sync-server.users = [ + { + username = "user"; + passwordFile = /etc/anki-sync-server/user; + } + ]; +} +``` + +Here, `passwordFile` is the path to a file containing just the password in +plaintext. Make sure to set permissions to make this file unreadable to any +user besides root. + +By default, the server listen address {option}`services.anki-sync-server.host` +is set to localhost, listening on port +{option}`services.anki-sync-server.port`, and does not open the firewall. This +is suitable for purely local testing, or to be used behind a reverse proxy. If +you want to expose the sync server directly to other computers (not recommended +in most circumstances, because the sync server doesn't use HTTPS), then set the +following options: + +``` +{ + services.anki-sync-server.host = "0.0.0.0"; + services.anki-sync-server.openFirewall = true; +} +``` + + +## Alternatives {#module-services-anki-sync-server-alternatives} + +The [`ankisyncd` NixOS +module](https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/misc/ankisyncd.nix) +provides similar functionality, but using a third-party implementation, +[`anki-sync-server-rs`](https://github.com/ankicommunity/anki-sync-server-rs/). +According to that project's README, it is "no longer maintained", and not +recommended for Anki 2.1.64+. diff --git a/nixos/modules/services/misc/anki-sync-server.nix b/nixos/modules/services/misc/anki-sync-server.nix new file mode 100644 index 0000000000000..a653820094176 --- /dev/null +++ b/nixos/modules/services/misc/anki-sync-server.nix @@ -0,0 +1,140 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.anki-sync-server; + name = "anki-sync-server"; + specEscape = replaceStrings ["%"] ["%%"]; + usersWithIndexes = + lists.imap1 (i: user: { + i = i; + user = user; + }) + cfg.users; + usersWithIndexesFile = filter (x: x.user.passwordFile != null) usersWithIndexes; + usersWithIndexesNoFile = filter (x: x.user.passwordFile == null && x.user.password != null) usersWithIndexes; + anki-sync-server-run = pkgs.writeShellScriptBin "anki-sync-server-run" '' + # When services.anki-sync-server.users.passwordFile is set, + # each password file is passed as a systemd credential, which is mounted in + # a file system exposed to the service. Here we read the passwords from + # the credential files to pass them as environment variables to the Anki + # sync server. + ${ + concatMapStringsSep + "\n" + (x: ''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:"''$(cat "''${CREDENTIALS_DIRECTORY}/"${escapeShellArg x.user.username})"'') + usersWithIndexesFile + } + # For users where services.anki-sync-server.users.password isn't set, + # export passwords in environment variables in plaintext. + ${ + concatMapStringsSep + "\n" + (x: ''export SYNC_USER${toString x.i}=${escapeShellArg x.user.username}:${escapeShellArg x.user.password}'') + usersWithIndexesNoFile + } + exec ${cfg.package}/bin/anki-sync-server + ''; +in { + options.services.anki-sync-server = { + enable = mkEnableOption "anki-sync-server"; + + package = mkPackageOption pkgs "anki-sync-server" { }; + + address = mkOption { + type = types.str; + default = "::1"; + description = '' + IP address anki-sync-server listens to. + Note host names are not resolved. + ''; + }; + + port = mkOption { + type = types.port; + default = 27701; + description = "Port number anki-sync-server listens to."; + }; + + openFirewall = mkOption { + default = false; + type = types.bool; + description = "Whether to open the firewall for the specified port."; + }; + + users = mkOption { + type = with types; + listOf (submodule { + options = { + username = mkOption { + type = str; + description = "User name accepted by anki-sync-server."; + }; + password = mkOption { + type = nullOr str; + default = null; + description = '' + Password accepted by anki-sync-server for the associated username. + **WARNING**: This option is **not secure**. This password will + be stored in *plaintext* and will be visible to *all users*. + See {option}`services.anki-sync-server.users.passwordFile` for + a more secure option. + ''; + }; + passwordFile = mkOption { + type = nullOr path; + default = null; + description = '' + File containing the password accepted by anki-sync-server for + the associated username. Make sure to make readable only by + root. + ''; + }; + }; + }); + description = "List of user-password pairs to provide to the sync server."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (builtins.length usersWithIndexesFile) + (builtins.length usersWithIndexesNoFile) > 0; + message = "At least one username-password pair must be set."; + } + ]; + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.port]; + + systemd.services.anki-sync-server = { + description = "anki-sync-server: Anki sync server built into Anki"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + path = [cfg.package]; + environment = { + SYNC_BASE = "%S/%N"; + SYNC_HOST = specEscape cfg.address; + SYNC_PORT = toString cfg.port; + }; + + serviceConfig = { + Type = "simple"; + DynamicUser = true; + StateDirectory = name; + ExecStart = "${anki-sync-server-run}/bin/anki-sync-server-run"; + Restart = "always"; + LoadCredential = + map + (x: "${specEscape x.user.username}:${specEscape (toString x.user.passwordFile)}") + usersWithIndexesFile; + }; + }; + }; + + meta = { + maintainers = with maintainers; [telotortium]; + doc = ./anki-sync-server.md; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 9caffa97ec8c1..1ed0f760c9a21 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -120,6 +120,7 @@ in { amazon-ssm-agent = handleTest ./amazon-ssm-agent.nix {}; amd-sev = runTest ./amd-sev.nix; anbox = runTest ./anbox.nix; + anki-sync-server = handleTest ./anki-sync-server.nix {}; anuko-time-tracker = handleTest ./anuko-time-tracker.nix {}; apcupsd = handleTest ./apcupsd.nix {}; apfs = runTest ./apfs.nix; diff --git a/nixos/tests/anki-sync-server.nix b/nixos/tests/anki-sync-server.nix new file mode 100644 index 0000000000000..7d08cc9cb878e --- /dev/null +++ b/nixos/tests/anki-sync-server.nix @@ -0,0 +1,71 @@ +import ./make-test-python.nix ({ pkgs, ... }: + let + ankiSyncTest = pkgs.writeScript "anki-sync-test.py" '' + #!${pkgs.python3}/bin/python + + import sys + + # get site paths from anki itself + from runpy import run_path + run_path("${pkgs.anki}/bin/.anki-wrapped") + import anki + + col = anki.collection.Collection('test_collection') + endpoint = 'http://localhost:27701' + + # Sanity check: verify bad login fails + try: + col.sync_login('baduser', 'badpass', endpoint) + print("bad user login worked?!") + sys.exit(1) + except anki.errors.SyncError: + pass + + # test logging in to users + col.sync_login('user', 'password', endpoint) + col.sync_login('passfileuser', 'passfilepassword', endpoint) + + # Test actual sync. login apparently doesn't remember the endpoint... + login = col.sync_login('user', 'password', endpoint) + login.endpoint = endpoint + sync = col.sync_collection(login, False) + assert sync.required == sync.NO_CHANGES + # TODO: create an archive with server content including a test card + # and check we got it? + ''; + testPasswordFile = pkgs.writeText "anki-password" "passfilepassword"; + in + { + name = "anki-sync-server"; + meta = with pkgs.lib.maintainers; { + maintainers = [ martinetd ]; + }; + + nodes.machine = { pkgs, ...}: { + services.anki-sync-server = { + enable = true; + users = [ + { username = "user"; + password = "password"; + } + { username = "passfileuser"; + passwordFile = testPasswordFile; + } + ]; + }; + }; + + + testScript = + '' + start_all() + + with subtest("Server starts successfully"): + # service won't start without users + machine.wait_for_unit("anki-sync-server.service") + machine.wait_for_open_port(27701) + + with subtest("Can sync"): + machine.succeed("${ankiSyncTest}") + ''; +}) |