1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 inherit (lib) mkOption types;
9 cfg = config.services.send;
10in
11{
12 options = {
13 services.send = {
14 enable = lib.mkEnableOption "Send, a file sharing web sevice for ffsend.";
15
16 package = lib.mkPackageOption pkgs "send" { };
17
18 environment = mkOption {
19 type =
20 with types;
21 attrsOf (
22 nullOr (oneOf [
23 bool
24 int
25 str
26 (listOf int)
27 ])
28 );
29 description = ''
30 All the available config options and their defaults can be found here: <https://github.com/timvisee/send/blob/master/server/config.js>,
31 some descriptions can found here: <https://github.com/timvisee/send/blob/master/docs/docker.md#environment-variables>
32
33 Values under {option}`services.send.environment` will override the predefined values in the Send service.
34 - Time/duration should be in seconds
35 - Filesize values should be in bytes
36 '';
37 example = {
38 DEFAULT_DOWNLOADS = 1;
39 DETECT_BASE_URL = true;
40 EXPIRE_TIMES_SECONDS = [
41 300
42 3600
43 86400
44 604800
45 ];
46 };
47 };
48
49 dataDir = lib.mkOption {
50 type = types.path;
51 readOnly = true;
52 default = "/var/lib/send";
53 description = ''
54 Directory for uploaded files.
55 Due to limitations in {option}`systemd.services.send.serviceConfig.DynamicUser`, this item is read only.
56 '';
57 };
58
59 baseUrl = mkOption {
60 type = types.nullOr types.str;
61 default = null;
62 description = ''
63 Base URL for the Send service.
64 Leave it blank to automatically detect the base url.
65 '';
66 };
67
68 host = lib.mkOption {
69 type = types.str;
70 default = "127.0.0.1";
71 description = "The hostname or IP address for Send to bind to.";
72 };
73
74 port = lib.mkOption {
75 type = types.port;
76 default = 1443;
77 description = "Port the Send service listens on.";
78 };
79
80 openFirewall = lib.mkOption {
81 type = types.bool;
82 default = false;
83 description = "Whether to open firewall ports for send";
84 };
85
86 redis = {
87 createLocally = lib.mkOption {
88 type = types.bool;
89 default = true;
90 description = "Whether to create a local redis automatically.";
91 };
92
93 name = lib.mkOption {
94 type = types.str;
95 default = "send";
96 description = ''
97 Name of the redis server.
98 Only used if {option}`services.send.redis.createLocally` is set to true.
99 '';
100 };
101
102 host = lib.mkOption {
103 type = types.str;
104 default = "localhost";
105 description = "Redis server address.";
106 };
107
108 port = lib.mkOption {
109 type = types.port;
110 default = 6379;
111 description = "Port of the redis server.";
112 };
113
114 passwordFile = mkOption {
115 type = types.nullOr types.path;
116 default = null;
117 example = "/run/agenix/send-redis-password";
118 description = ''
119 The path to the file containing the Redis password.
120
121 If {option}`services.send.redis.createLocally` is set to true,
122 the content of this file will be used as the password for the locally created Redis instance.
123
124 Leave it blank if no password is required.
125 '';
126 };
127 };
128 };
129 };
130
131 config = lib.mkIf cfg.enable {
132
133 services.send.environment.DETECT_BASE_URL = cfg.baseUrl == null;
134
135 assertions = [
136 {
137 assertion = cfg.redis.createLocally -> cfg.redis.host == "localhost";
138 message = "the redis host must be localhost if services.send.redis.createLocally is set to true";
139 }
140 ];
141
142 networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.port;
143
144 services.redis = lib.optionalAttrs cfg.redis.createLocally {
145 servers."${cfg.redis.name}" = {
146 enable = true;
147 bind = "localhost";
148 port = cfg.redis.port;
149 };
150 };
151
152 systemd.services.send = {
153 serviceConfig = {
154 Type = "simple";
155 Restart = "always";
156 StateDirectory = "send";
157 WorkingDirectory = cfg.dataDir;
158 ReadWritePaths = cfg.dataDir;
159 LoadCredential = lib.optionalString (
160 cfg.redis.passwordFile != null
161 ) "redis-password:${cfg.redis.passwordFile}";
162
163 # Hardening
164 RestrictAddressFamilies = [
165 "AF_UNIX"
166 "AF_INET"
167 "AF_INET6"
168 ];
169 AmbientCapabilities = lib.optionalString (cfg.port < 1024) "cap_net_bind_service";
170 DynamicUser = true;
171 CapabilityBoundingSet = "";
172 NoNewPrivileges = true;
173 RemoveIPC = true;
174 PrivateTmp = true;
175 ProcSubset = "pid";
176 ProtectClock = true;
177 ProtectControlGroups = true;
178 ProtectHome = true;
179 ProtectHostname = true;
180 ProtectKernelLogs = true;
181 ProtectKernelModules = true;
182 ProtectKernelTunables = true;
183 ProtectProc = "invisible";
184 ProtectSystem = "full";
185 RestrictNamespaces = true;
186 RestrictRealtime = true;
187 RestrictSUIDSGID = true;
188 SystemCallArchitectures = "native";
189 UMask = "0077";
190 };
191 environment = {
192 IP_ADDRESS = cfg.host;
193 PORT = toString cfg.port;
194 BASE_URL = if (cfg.baseUrl == null) then "http://${cfg.host}:${toString cfg.port}" else cfg.baseUrl;
195 FILE_DIR = cfg.dataDir + "/uploads";
196 REDIS_HOST = cfg.redis.host;
197 REDIS_PORT = toString cfg.redis.port;
198 }
199 // (lib.mapAttrs (
200 name: value:
201 if lib.isList value then
202 "[" + lib.concatStringsSep ", " (map (x: toString x) value) + "]"
203 else if lib.isBool value then
204 lib.boolToString value
205 else
206 toString value
207 ) cfg.environment);
208 after = [
209 "network.target"
210 ]
211 ++ lib.optionals cfg.redis.createLocally [
212 "redis-${cfg.redis.name}.service"
213 ];
214 description = "Send web service";
215 wantedBy = [ "multi-user.target" ];
216 script = ''
217 ${lib.optionalString (cfg.redis.passwordFile != null) ''
218 export REDIS_PASSWORD="$(cat $CREDENTIALS_DIRECTORY/redis-password)"
219 ''}
220 ${lib.getExe cfg.package}
221 '';
222 };
223 };
224
225 meta.maintainers = with lib.maintainers; [ moraxyc ];
226}