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 {
193 IP_ADDRESS = cfg.host;
194 PORT = toString cfg.port;
195 BASE_URL = if (cfg.baseUrl == null) then "http://${cfg.host}:${toString cfg.port}" else cfg.baseUrl;
196 FILE_DIR = cfg.dataDir + "/uploads";
197 REDIS_HOST = cfg.redis.host;
198 REDIS_PORT = toString cfg.redis.port;
199 }
200 // (lib.mapAttrs (
201 name: value:
202 if lib.isList value then
203 "[" + lib.concatStringsSep ", " (map (x: toString x) value) + "]"
204 else if lib.isBool value then
205 lib.boolToString value
206 else
207 toString value
208 ) cfg.environment);
209 after =
210 [
211 "network.target"
212 ]
213 ++ lib.optionals cfg.redis.createLocally [
214 "redis-${cfg.redis.name}.service"
215 ];
216 description = "Send web service";
217 wantedBy = [ "multi-user.target" ];
218 script = ''
219 ${lib.optionalString (cfg.redis.passwordFile != null) ''
220 export REDIS_PASSWORD="$(cat $CREDENTIALS_DIRECTORY/redis-password)"
221 ''}
222 ${lib.getExe cfg.package}
223 '';
224 };
225 };
226
227 meta.maintainers = with lib.maintainers; [ moraxyc ];
228}