···96969797- [atuin](https://github.com/ellie/atuin), a sync server for shell history. Available as [services.atuin](#opt-services.atuin.enable).
98989999+- [SFTPGo](https://github.com/drakkan/sftpgo), a fully featured and highly configurable SFTP server with optional HTTP/S, FTP/S and WebDAV support. Available as [services.sftpgo](options.html#opt-services.sftpgo.enable).
100100+99101- [esphome](https://esphome.io), a dashboard to configure ESP8266/ESP32 devices for use with Home Automation systems. Available as [services.esphome](#opt-services.esphome.enable).
100102101103- [networkd-dispatcher](https://gitlab.com/craftyguy/networkd-dispatcher), a dispatcher service for systemd-networkd connection status changes. Available as [services.networkd-dispatcher](#opt-services.networkd-dispatcher.enable).
···566568- `gitea` module options have been changed to be RFC042 conforming (i.e. some options were moved to be located under `services.gitea.settings`)
567569568570- `boot.initrd.luks.device.<name>` has a new `tryEmptyPassphrase` option, this is useful for OEM's who need to install an encrypted disk with a future settable passphrase
571571+572572+- there is a new `boot/stratisroot.nix` module that enables booting from a volume managed by the Stratis storage management daemon. Use `fileSystems.<name>.stratis.poolUuid` to configure the pool containing the fs.
569573570574- Lisp gained a [manual section](https://nixos.org/manual/nixpkgs/stable/#lisp), documenting a new and backwards incompatible interface. The previous interface will be removed in a future release.
571575
···3636 description = lib.mdDoc "Location of the mounted file system.";
3737 };
38383939+ stratis.poolUuid = lib.mkOption {
4040+ type = types.uniq (types.nullOr types.str);
4141+ description = lib.mdDoc ''
4242+ UUID of the stratis pool that the fs is located in
4343+ '';
4444+ example = "04c68063-90a5-4235-b9dd-6180098a20d9";
4545+ default = null;
4646+ };
4747+3948 device = mkOption {
4049 default = null;
4150 example = "/dev/sda";
···3344 meta = {
55 maintainers = with lib.maintainers; [ OPNA2608 ];
66- # Natively running Mir has problems with capturing the first registered libinput device.
77- # In our VM runners on ARM and on some hardware configs (my RPi4, distro-independent), this misses the keyboard.
88- # It can be worked around by dis- and reconnecting the affected hardware, but we can't do this in these tests.
99- # https://github.com/MirServer/mir/issues/2837
1010- broken = pkgs.stdenv.hostPlatform.isAarch;
116 };
127138 nodes.machine = { config, ... }: {
+384
nixos/tests/sftpgo.nix
···11+# SFTPGo NixOS test
22+#
33+# This NixOS test sets up a basic test scenario for the SFTPGo module
44+# and covers the following scenarios:
55+# - uploading a file via sftp
66+# - downloading the file over sftp
77+# - assert that the ACLs are respected
88+# - share a file between alice and bob (using sftp)
99+# - assert that eve cannot acceess the shared folder between alice and bob.
1010+#
1111+# Additional test coverage for the remaining protocols (i.e. ftp, http and webdav)
1212+# would be a nice to have for the future.
1313+{ pkgs, lib, ... }:
1414+1515+with lib;
1616+1717+let
1818+ inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
1919+2020+ # Returns an attributeset of users who are not system users.
2121+ normalUsers = config:
2222+ filterAttrs (name: user: user.isNormalUser) config.users.users;
2323+2424+ # Returns true if a user is a member of the given group
2525+ isMemberOf =
2626+ config:
2727+ # str
2828+ groupName:
2929+ # users.users attrset
3030+ user:
3131+ any (x: x == user.name) config.users.groups.${groupName}.members;
3232+3333+ # Generates a valid SFTPGo user configuration for a given user
3434+ # Will be converted to JSON and loaded on application startup.
3535+ generateUserAttrSet =
3636+ config:
3737+ # attrset returned by config.users.users.<username>
3838+ user: {
3939+ # 0: user is disabled, login is not allowed
4040+ # 1: user is enabled
4141+ status = 1;
4242+4343+ username = user.name;
4444+ password = ""; # disables password authentication
4545+ public_keys = user.openssh.authorizedKeys.keys;
4646+ email = "${user.name}@example.com";
4747+4848+ # User home directory on the local filesystem
4949+ home_dir = "${config.services.sftpgo.dataDir}/users/${user.name}";
5050+5151+ # Defines a mapping between virtual SFTPGo paths and filesystem paths outside the user home directory.
5252+ #
5353+ # Supported for local filesystem only. If one or more of the specified folders are not
5454+ # inside the dataprovider they will be automatically created.
5555+ # You have to create the folder on the filesystem yourself
5656+ virtual_folders =
5757+ optional (isMemberOf config sharedFolderName user) {
5858+ name = sharedFolderName;
5959+ mapped_path = "${config.services.sftpgo.dataDir}/${sharedFolderName}";
6060+ virtual_path = "/${sharedFolderName}";
6161+ };
6262+6363+ # Defines the ACL on the virtual filesystem
6464+ permissions =
6565+ recursiveUpdate {
6666+ "/" = [ "list" ]; # read-only top level directory
6767+ "/private" = [ "*" ]; # private subdirectory, not shared with others
6868+ } (optionalAttrs (isMemberOf config "shared" user) {
6969+ "/shared" = [ "*" ];
7070+ });
7171+7272+ filters = {
7373+ allowed_ip = [];
7474+ denied_ip = [];
7575+ web_client = [
7676+ "password-change-disabled"
7777+ "password-reset-disabled"
7878+ "api-key-auth-change-disabled"
7979+ ];
8080+ };
8181+8282+ upload_bandwidth = 0; # unlimited
8383+ download_bandwidth = 0; # unlimited
8484+ expiration_date = 0; # means no expiration
8585+ max_sessions = 0;
8686+ quota_size = 0;
8787+ quota_files = 0;
8888+ };
8989+9090+ # Generates a json file containing a static configuration
9191+ # of users and folders to import to SFTPGo.
9292+ loadDataJson = config: pkgs.writeText "users-and-folders.json" (builtins.toJSON {
9393+ users =
9494+ mapAttrsToList (name: user: generateUserAttrSet config user) (normalUsers config);
9595+9696+ folders = [
9797+ {
9898+ name = sharedFolderName;
9999+ description = "shared folder";
100100+101101+ # 0: local filesystem
102102+ # 1: AWS S3 compatible
103103+ # 2: Google Cloud Storage
104104+ filesystem.provider = 0;
105105+106106+ # Mapped path on the local filesystem
107107+ mapped_path = "${config.services.sftpgo.dataDir}/${sharedFolderName}";
108108+109109+ # All users in the matching group gain access
110110+ users = config.users.groups.${sharedFolderName}.members;
111111+ }
112112+ ];
113113+ });
114114+115115+ # Generated Host Key for connecting to SFTPGo's sftp subsystem.
116116+ snakeOilHostKey = pkgs.writeText "sftpgo_ed25519_host_key" ''
117117+ -----BEGIN OPENSSH PRIVATE KEY-----
118118+ b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
119119+ QyNTUxOQAAACBOtQu6U135yxtrvUqPoozUymkjoNNPVK6rqjS936RLtQAAAJAXOMoSFzjK
120120+ EgAAAAtzc2gtZWQyNTUxOQAAACBOtQu6U135yxtrvUqPoozUymkjoNNPVK6rqjS936RLtQ
121121+ AAAEAoRLEV1VD80mg314ObySpfrCcUqtWoOSS3EtMPPhx08U61C7pTXfnLG2u9So+ijNTK
122122+ aSOg009UrquqNL3fpEu1AAAADHNmdHBnb0BuaXhvcwE=
123123+ -----END OPENSSH PRIVATE KEY-----
124124+ '';
125125+126126+ adminUsername = "admin";
127127+ adminPassword = "secretadminpassword";
128128+ aliceUsername = "alice";
129129+ alicePassword = "secretalicepassword";
130130+ bobUsername = "bob";
131131+ bobPassword = "secretbobpassword";
132132+ eveUsername = "eve";
133133+ evePassword = "secretevepassword";
134134+ sharedFolderName = "shared";
135135+136136+ # A file for testing uploading via SFTP
137137+ testFile = pkgs.writeText "test.txt" "hello world";
138138+ sharedFile = pkgs.writeText "shared.txt" "shared content";
139139+140140+ # Define the for exposing SFTP
141141+ sftpPort = 2022;
142142+143143+ # Define the for exposing HTTP
144144+ httpPort = 8080;
145145+in
146146+{
147147+ name = "sftpgo";
148148+149149+ meta.maintainers = with maintainers; [ yayayayaka ];
150150+151151+ nodes = {
152152+ server = { nodes, ... }: {
153153+ networking.firewall.allowedTCPPorts = [ sftpPort httpPort ];
154154+155155+ # nodes.server.configure postgresql database
156156+ services.postgresql = {
157157+ enable = true;
158158+ ensureDatabases = [ "sftpgo" ];
159159+ ensureUsers = [{
160160+ name = "sftpgo";
161161+ ensurePermissions."DATABASE sftpgo" = "ALL PRIVILEGES";
162162+ }];
163163+ };
164164+165165+ services.sftpgo = {
166166+ enable = true;
167167+168168+ loadDataFile = (loadDataJson nodes.server);
169169+170170+ settings = {
171171+ data_provider = {
172172+ driver = "postgresql";
173173+ name = "sftpgo";
174174+ username = "sftpgo";
175175+ host = "/run/postgresql";
176176+ port = 5432;
177177+178178+ # Enables the possibility to create an initial admin user on first startup.
179179+ create_default_admin = true;
180180+ };
181181+182182+ httpd.bindings = [
183183+ {
184184+ address = ""; # listen on all interfaces
185185+ port = httpPort;
186186+ enable_https = false;
187187+188188+ enable_web_client = true;
189189+ enable_web_admin = true;
190190+ }
191191+ ];
192192+193193+ # Enable sftpd
194194+ sftpd = {
195195+ bindings = [{
196196+ address = ""; # listen on all interfaces
197197+ port = sftpPort;
198198+ }];
199199+ host_keys = [ snakeOilHostKey ];
200200+ password_authentication = false;
201201+ keyboard_interactive_authentication = false;
202202+ };
203203+ };
204204+ };
205205+206206+ systemd.services.sftpgo = {
207207+ after = [ "postgresql.service"];
208208+ environment = {
209209+ # Update existing users
210210+ SFTPGO_LOADDATA_MODE = "0";
211211+ SFTPGO_DEFAULT_ADMIN_USERNAME = adminUsername;
212212+213213+ # This will end up in cleartext in the systemd service.
214214+ # Don't use this approach in production!
215215+ SFTPGO_DEFAULT_ADMIN_PASSWORD = adminPassword;
216216+ };
217217+ };
218218+219219+ # Sets up the folder hierarchy on the local filesystem
220220+ systemd.tmpfiles.rules =
221221+ let
222222+ sftpgoUser = nodes.server.services.sftpgo.user;
223223+ sftpgoGroup = nodes.server.services.sftpgo.group;
224224+ statePath = nodes.server.services.sftpgo.dataDir;
225225+ in [
226226+ # Create state directory
227227+ "d ${statePath} 0750 ${sftpgoUser} ${sftpgoGroup} -"
228228+ "d ${statePath}/users 0750 ${sftpgoUser} ${sftpgoGroup} -"
229229+230230+ # Created shared folder directories
231231+ "d ${statePath}/${sharedFolderName} 2770 ${sftpgoUser} ${sharedFolderName} -"
232232+ ]
233233+ ++ mapAttrsToList (name: user:
234234+ # Create private user directories
235235+ ''
236236+ d ${statePath}/users/${user.name} 0700 ${sftpgoUser} ${sftpgoGroup} -
237237+ d ${statePath}/users/${user.name}/private 0700 ${sftpgoUser} ${sftpgoGroup} -
238238+ ''
239239+ ) (normalUsers nodes.server);
240240+241241+ users.users =
242242+ let
243243+ commonAttrs = {
244244+ isNormalUser = true;
245245+ openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
246246+ };
247247+ in {
248248+ # SFTPGo admin user
249249+ admin = commonAttrs // {
250250+ password = adminPassword;
251251+ };
252252+253253+ # Alice and bob share folders with each other
254254+ alice = commonAttrs // {
255255+ password = alicePassword;
256256+ extraGroups = [ sharedFolderName ];
257257+ };
258258+259259+ bob = commonAttrs // {
260260+ password = bobPassword;
261261+ extraGroups = [ sharedFolderName ];
262262+ };
263263+264264+ # Eve has no shared folders
265265+ eve = commonAttrs // {
266266+ password = evePassword;
267267+ };
268268+ };
269269+270270+ users.groups.${sharedFolderName} = {};
271271+272272+ specialisation = {
273273+ # A specialisation for asserting that SFTPGo can bind to privileged ports.
274274+ privilegedPorts.configuration = { ... }: {
275275+ networking.firewall.allowedTCPPorts = [ 22 80 ];
276276+ services.sftpgo = {
277277+ settings = {
278278+ sftpd.bindings = mkForce [{
279279+ address = "";
280280+ port = 22;
281281+ }];
282282+283283+ httpd.bindings = mkForce [{
284284+ address = "";
285285+ port = 80;
286286+ }];
287287+ };
288288+ };
289289+ };
290290+ };
291291+ };
292292+293293+ client = { nodes, ... }: {
294294+ # Add the SFTPGo host key to the global known_hosts file
295295+ programs.ssh.knownHosts =
296296+ let
297297+ commonAttrs = {
298298+ publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE61C7pTXfnLG2u9So+ijNTKaSOg009UrquqNL3fpEu1";
299299+ };
300300+ in {
301301+ "server" = commonAttrs;
302302+ "[server]:2022" = commonAttrs;
303303+ };
304304+ };
305305+ };
306306+307307+ testScript = { nodes, ... }: let
308308+ # A function to generate test cases for wheter
309309+ # a specified username is expected to access the shared folder.
310310+ accessSharedFoldersSubtest =
311311+ { # The username to run as
312312+ username
313313+ # Whether the tests are expected to succeed or not
314314+ , shouldSucceed ? true
315315+ }: ''
316316+ with subtest("Test whether ${username} can access shared folders"):
317317+ client.${if shouldSucceed then "succeed" else "fail"}("sftp -P ${toString sftpPort} -b ${
318318+ pkgs.writeText "${username}-ls-${sharedFolderName}" ''
319319+ ls ${sharedFolderName}
320320+ ''
321321+ } ${username}@server")
322322+ '';
323323+ statePath = nodes.server.services.sftpgo.dataDir;
324324+ in ''
325325+ start_all()
326326+327327+ client.wait_for_unit("default.target")
328328+ server.wait_for_unit("sftpgo.service")
329329+330330+ with subtest("web client"):
331331+ client.wait_until_succeeds("curl -sSf http://server:${toString httpPort}/web/client/login")
332332+333333+ # Ensure sftpgo found the static folder
334334+ client.wait_until_succeeds("curl -o /dev/null -sSf http://server:${toString httpPort}/static/favicon.ico")
335335+336336+ with subtest("Setup SSH keys"):
337337+ client.succeed("mkdir -m 700 /root/.ssh")
338338+ client.succeed("cat ${snakeOilPrivateKey} > /root/.ssh/id_ecdsa")
339339+ client.succeed("chmod 600 /root/.ssh/id_ecdsa")
340340+341341+ with subtest("Copy a file over sftp"):
342342+ client.wait_until_succeeds("scp -P ${toString sftpPort} ${toString testFile} alice@server:/private/${testFile.name}")
343343+ server.succeed("test -s ${statePath}/users/alice/private/${testFile.name}")
344344+345345+ # The configured ACL should prevent uploading files to the root directory
346346+ client.fail("scp -P ${toString sftpPort} ${toString testFile} alice@server:/")
347347+348348+ with subtest("Attempting an interactive SSH sessions must fail"):
349349+ client.fail("ssh -p ${toString sftpPort} alice@server")
350350+351351+ ${accessSharedFoldersSubtest {
352352+ username = "alice";
353353+ shouldSucceed = true;
354354+ }}
355355+356356+ ${accessSharedFoldersSubtest {
357357+ username = "bob";
358358+ shouldSucceed = true;
359359+ }}
360360+361361+ ${accessSharedFoldersSubtest {
362362+ username = "eve";
363363+ shouldSucceed = false;
364364+ }}
365365+366366+ with subtest("Test sharing files"):
367367+ # Alice uploads a file to shared folder
368368+ client.succeed("scp -P ${toString sftpPort} ${toString sharedFile} alice@server:/${sharedFolderName}/${sharedFile.name}")
369369+ server.succeed("test -s ${statePath}/${sharedFolderName}/${sharedFile.name}")
370370+371371+ # Bob downloads the file from shared folder
372372+ client.succeed("scp -P ${toString sftpPort} bob@server:/shared/${sharedFile.name} ${sharedFile.name}")
373373+ client.succeed("test -s ${sharedFile.name}")
374374+375375+ # Eve should not get the file from shared folder
376376+ client.fail("scp -P ${toString sftpPort} eve@server:/shared/${sharedFile.name}")
377377+378378+ server.succeed("/run/current-system/specialisation/privilegedPorts/bin/switch-to-configuration test")
379379+380380+ client.wait_until_succeeds("sftp -P 22 -b ${pkgs.writeText "get-hello-world.txt" ''
381381+ get /private/${testFile.name}
382382+ ''} alice@server")
383383+ '';
384384+}
···1313 targetFile: new URL("default.nix", import.meta.url).pathname,
1414};
15151616+async function utf16ToUtf8(blob) {
1717+ // Sometime, upstream saves the SHA256SUMS.txt file in UTF-16, which absolutely breaks node's string handling
1818+ // So we need to convert this blob to UTF-8
1919+2020+ // We need to skip the first 2 bytes, which are the BOM
2121+ const arrayBuffer = await blob.slice(2).arrayBuffer();
2222+ const buffer = Buffer.from(arrayBuffer);
2323+ const utf8String = buffer.toString('utf16le');
2424+ return utf8String;
2525+}
2626+1627async function getLatestVersion() {
1728 const requestResult = await fetch(constants.githubUrl);
1829 if (!requestResult.ok) {
···37483849 let sha256 = hashFileContent.
3950 split('\n').
5151+ map(line => line.replace("\r", "")). // Side-effect of the UTF-16 conversion, if the file was created from Windows
4052 filter((line) => line.endsWith(targetFile))[0].
4153 split(' ')[0];
4254···4759 // Upstream provides a file with the hashes of the files, but it's not in the SRI format, and it refers to the compressed tarball
4860 // So let's just use nix-prefetch-url to get the hashes of the decompressed tarball, and `nix hash to-sri` to convert them to SRI format
4961 const hashFileUrl = constants.sha256FileURL(newVersion);
5050- const hashFileContent = await fetch(hashFileUrl).then((response) => response.text());
6262+ const hashFileContent = await fetch(hashFileUrl).then((response) => response.blob());
6363+ const headerbuffer = await hashFileContent.slice(0, 2).arrayBuffer()
6464+ const header = Buffer.from(headerbuffer).toString('hex');
6565+6666+ // We must detect if it's UTF-16 or UTF-8. If it's UTF-16, we must convert it to UTF-8, otherwise just use it as-is
6767+ const hashFileContentString = header == 'fffe' ?
6868+ await utf16ToUtf8(hashFileContent) :
6969+ await hashFileContent.text();
51705271 let x86_64;
5372 let aarch64;
5473 console.log("Getting new hashes");
5574 let promises = [
5656- getSha256Sum(hashFileContent, constants.x86_64FileName(newVersion)).then((hash) => { x86_64 = hash; }),
5757- getSha256Sum(hashFileContent, constants.aarch64FileName(newVersion)).then((hash) => { aarch64 = hash; }),
7575+ getSha256Sum(hashFileContentString, constants.x86_64FileName(newVersion)).then((hash) => { x86_64 = hash; }),
7676+ getSha256Sum(hashFileContentString, constants.aarch64FileName(newVersion)).then((hash) => { aarch64 = hash; }),
5877 ];
5978 await Promise.all(promises);
6079 return { x86_64, aarch64 };
···77# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8899[dependencies]
1010+eyre = "0.6.8"
1011goblin = "0.5.0"
···11+import ./generic.nix {
22+ version = "0.1.0-beta.3";
33+ hash = "sha256-0p+1cMn9PU+Jk2JW7G+sdzxhMaI3gEAk5w2nm05oBSU=";
44+ outputHashes = {
55+ "uniffi-0.21.0" = "sha256-blKCfCsSNtr8NtO7Let7VJ/9oGuW9Eu8j9A6/oHUcP0=";
66+ };
77+ cargoLock = ./Cargo-beta.3.lock;
88+ patches = [
99+ # This is needed because two versions of indexed_db_futures are present (which will fail to vendor, see https://github.com/rust-lang/cargo/issues/10310).
1010+ # (matrix-sdk-crypto-nodejs doesn't use this dependency, we only need to remove it to vendor the dependencies successfully.)
1111+ ./remove-duplicate-dependency.patch
1212+ ];
1313+}
···5252 '';
53535454 cmakeFlags = [
5555+ "-DODBC_LIB_DIR=${lib.getLib unixODBC}/lib"
5656+ "-DODBC_INCLUDE_DIR=${lib.getDev unixODBC}/include"
5557 "-DWITH_OPENSSL=ON"
5658 # on darwin this defaults to ON but we want to build against unixODBC
5759 "-DWITH_IODBC=OFF"