1{
2 lib,
3 config,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.onlyoffice;
10in
11{
12 options.services.onlyoffice = {
13 enable = lib.mkEnableOption "OnlyOffice DocumentServer";
14
15 enableExampleServer = lib.mkEnableOption "OnlyOffice example server";
16
17 hostname = lib.mkOption {
18 type = lib.types.str;
19 default = "localhost";
20 description = "FQDN for the OnlyOffice instance.";
21 };
22
23 jwtSecretFile = lib.mkOption {
24 type = lib.types.nullOr lib.types.str;
25 default = null;
26 description = ''
27 Path to a file that contains the secret to sign web requests using JSON Web Tokens.
28 If left at the default value null signing is disabled.
29 '';
30 };
31
32 package = lib.mkPackageOption pkgs "onlyoffice-documentserver" { };
33
34 x2t = lib.mkPackageOption pkgs "x2t" { };
35
36 port = lib.mkOption {
37 type = lib.types.port;
38 default = 8000;
39 description = "Port the OnlyOffice document server should listen on.";
40 };
41
42 examplePort = lib.mkOption {
43 type = lib.types.port;
44 default = null;
45 description = "Port the OnlyOffice example server should listen on.";
46 };
47
48 postgresHost = lib.mkOption {
49 type = lib.types.str;
50 default = "/run/postgresql";
51 description = "The Postgresql hostname or socket path OnlyOffice should connect to.";
52 };
53
54 postgresName = lib.mkOption {
55 type = lib.types.str;
56 default = "onlyoffice";
57 description = "The name of database OnlyOffice should use.";
58 };
59
60 postgresPasswordFile = lib.mkOption {
61 type = lib.types.nullOr lib.types.str;
62 default = null;
63 description = ''
64 Path to a file that contains the password OnlyOffice should use to connect to Postgresql.
65 Unused when using socket authentication.
66 '';
67 };
68
69 postgresUser = lib.mkOption {
70 type = lib.types.str;
71 default = "onlyoffice";
72 description = ''
73 The username OnlyOffice should use to connect to Postgresql.
74 Unused when using socket authentication.
75 '';
76 };
77
78 rabbitmqUrl = lib.mkOption {
79 type = lib.types.str;
80 default = "amqp://guest:guest@localhost:5672";
81 description = "The Rabbitmq in amqp URI style OnlyOffice should connect to.";
82 };
83 };
84
85 config = lib.mkIf cfg.enable {
86 services = {
87 nginx = {
88 enable = lib.mkDefault true;
89 # misses text/csv, font/ttf, application/x-font-ttf, application/rtf, application/wasm
90 recommendedGzipSettings = lib.mkDefault true;
91 recommendedProxySettings = lib.mkDefault true;
92
93 upstreams = {
94 # /etc/nginx/includes/http-common.conf
95 onlyoffice-docservice = {
96 servers = {
97 "localhost:${toString cfg.port}" = { };
98 };
99 };
100 onlyoffice-example = lib.mkIf cfg.enableExampleServer {
101 servers = {
102 "localhost:${toString cfg.examplePort}" = { };
103 };
104 };
105 };
106
107 virtualHosts.${cfg.hostname} = {
108 locations = {
109 # resources that are generated and thus cannot be taken from the cfg.package yet:
110 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(sdkjs/common/AllFonts.js)$".extraConfig = ''
111 proxy_pass http://onlyoffice-docservice/$2$3;
112 '';
113 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(fonts/.*)$".extraConfig = ''
114 proxy_pass http://onlyoffice-docservice/$2$3;
115 '';
116 # /etc/nginx/includes/ds-docservice.conf
117 # disable caching for api.js
118 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(web-apps\\/apps\\/api\\/documents\\/api\\.js)$".extraConfig =
119 ''
120 expires -1;
121 # gzip_static on;
122 alias ${cfg.package}/var/www/onlyoffice/documentserver/$2;
123 '';
124 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(document_editor_service_worker\\.js)$".extraConfig =
125 ''
126 expires 365d;
127 alias ${cfg.package}/var/www/onlyoffice/documentserver/sdkjs/common/serviceworker/$2;
128 '';
129 # suppress logging the unsupported locale error in web-apps
130 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(web-apps)(\\/.*\\.json)$".extraConfig = ''
131 expires 365d;
132 error_log /dev/null crit;
133 alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
134 '';
135 # suppress logging the unsupported locale error in plugins
136 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(sdkjs-plugins)(\\/.*\\.json)$".extraConfig = ''
137 expires 365d;
138 error_log /dev/null crit;
139 alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
140 '';
141 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(web-apps|sdkjs|sdkjs-plugins|fonts|dictionaries)(\\/.*)$".extraConfig =
142 ''
143 expires 365d;
144 alias ${cfg.package}/var/www/onlyoffice/documentserver/$2$3;
145 '';
146 "~* ^(\\/cache\\/files.*)(\\/.*)".extraConfig = ''
147 alias /var/lib/onlyoffice/documentserver/App_Data$1;
148 add_header Content-Disposition "attachment; filename*=UTF-8''$arg_filename";
149
150 set $secure_link_secret verysecretstring;
151 secure_link $arg_md5,$arg_expires;
152 secure_link_md5 "$secure_link_expires$uri$secure_link_secret";
153
154 if ($secure_link = "") {
155 return 403;
156 }
157
158 if ($secure_link = "0") {
159 return 410;
160 }
161 '';
162 # Allow "/internal" interface only from 127.0.0.1
163 # Don't comment out the section below for the security reason!
164 "~* ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(internal)(\\/.*)$".extraConfig = ''
165 allow 127.0.0.1;
166 deny all;
167 proxy_pass http://onlyoffice-docservice/$2$3;
168 '';
169 # Allow "/info" interface only from 127.0.0.1 by default
170 # Comment out lines allow 127.0.0.1; and deny all;
171 # of below section to turn on the info page
172 "~* ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?\\/(info)(\\/.*)$".extraConfig = ''
173 allow 127.0.0.1;
174 deny all;
175 proxy_pass http://onlyoffice-docservice/$2$3;
176 '';
177 "/".extraConfig = ''
178 proxy_pass http://onlyoffice-docservice;
179 '';
180 "~ ^(\\/[\\d]+\\.[\\d]+\\.[\\d]+[\\.|-][\\w]+)?(\\/(doc|downloadas)\\/.*)".extraConfig = ''
181 proxy_pass http://onlyoffice-docservice$2$is_args$args;
182 proxy_http_version 1.1;
183 '';
184 # end of /etc/nginx/includes/ds-docservice.conf
185 "/${cfg.package.version}/".extraConfig = ''
186 proxy_pass http://onlyoffice-docservice/;
187 '';
188 # /etc/nginx/includes/ds-example.conf
189 "~ ^(\\/welcome\\/.*)$".extraConfig = lib.mkIf cfg.enableExampleServer ''
190 expires 365d;
191 alias ${cfg.package}/var/www/onlyoffice/documentserver-example$1;
192 index docker.html;
193 '';
194 "/example/".extraConfig = lib.mkIf cfg.enableExampleServer ''
195 proxy_pass http://onlyoffice-example/;
196 proxy_set_header X-Forwarded-Path /example;
197 '';
198 };
199 extraConfig = ''
200 rewrite ^/$ /welcome/ redirect;
201 rewrite ^\/OfficeWeb(\/apps\/.*)$ /${cfg.package.version}/web-apps$1 redirect;
202 rewrite ^(\/web-apps\/apps\/(?!api\/).*)$ /${cfg.package.version}$1 redirect;
203
204 # based on https://github.com/ONLYOFFICE/document-server-package/blob/master/common/documentserver/nginx/includes/http-common.conf.m4#L29-L34
205 # without variable indirection and correct variable names
206 proxy_set_header Host $host;
207 proxy_set_header X-Forwarded-Host $host;
208 proxy_set_header X-Forwarded-Proto $scheme;
209 # required for CSP to take effect
210 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
211 # required for websocket
212 proxy_set_header Upgrade $http_upgrade;
213 proxy_set_header Connection $connection_upgrade;
214 '';
215 };
216 };
217
218 rabbitmq.enable = lib.mkDefault true;
219
220 postgresql = {
221 enable = lib.mkDefault true;
222 ensureDatabases = [ "onlyoffice" ];
223 ensureUsers = [
224 {
225 name = "onlyoffice";
226 ensureDBOwnership = true;
227 }
228 ];
229 };
230 };
231
232 systemd.services = {
233 onlyoffice-converter = {
234 description = "onlyoffice converter";
235 after = [
236 "network.target"
237 "onlyoffice-docservice.service"
238 "postgresql.service"
239 ];
240 requires = [
241 "network.target"
242 "onlyoffice-docservice.service"
243 "postgresql.service"
244 ];
245 wantedBy = [ "multi-user.target" ];
246 serviceConfig = {
247 ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper FileConverter/converter /run/onlyoffice/config";
248 Group = "onlyoffice";
249 Restart = "always";
250 RuntimeDirectory = "onlyoffice";
251 StateDirectory = "onlyoffice";
252 Type = "simple";
253 User = "onlyoffice";
254 };
255 };
256
257 onlyoffice-docservice =
258 let
259 onlyoffice-prestart = pkgs.writeShellScript "onlyoffice-prestart" ''
260 PATH=$PATH:${
261 lib.makeBinPath (
262 with pkgs;
263 [
264 jq
265 moreutils
266 config.services.postgresql.package
267 ]
268 )
269 }
270 umask 077
271 mkdir -p /run/onlyoffice/config/ /var/lib/onlyoffice/documentserver/sdkjs/{slide/themes,common}/ /var/lib/onlyoffice/documentserver/{fonts,server/FileConverter/bin}/
272 cp -r ${cfg.package}/etc/onlyoffice/documentserver/* /run/onlyoffice/config/
273 chmod u+w /run/onlyoffice/config/default.json
274
275 # Allow members of the onlyoffice group to serve files under /var/lib/onlyoffice/documentserver/App_Data
276 chmod g+x /var/lib/onlyoffice/documentserver
277
278 cp /run/onlyoffice/config/default.json{,.orig}
279
280 # for a mapping of environment variables from the docker container to json options see
281 # https://github.com/ONLYOFFICE/Docker-DocumentServer/blob/master/run-document-server.sh
282 jq '
283 .services.CoAuthoring.server.port = ${toString cfg.port} |
284 .services.CoAuthoring.sql.dbHost = "${cfg.postgresHost}" |
285 .services.CoAuthoring.sql.dbName = "${cfg.postgresName}" |
286 ${lib.optionalString (cfg.postgresPasswordFile != null) ''
287 .services.CoAuthoring.sql.dbPass = "'"$(cat ${cfg.postgresPasswordFile})"'" |
288 ''}
289 .services.CoAuthoring.sql.dbUser = "${cfg.postgresUser}" |
290 ${lib.optionalString (cfg.jwtSecretFile != null) ''
291 .services.CoAuthoring.token.enable.browser = true |
292 .services.CoAuthoring.token.enable.request.inbox = true |
293 .services.CoAuthoring.token.enable.request.outbox = true |
294 .services.CoAuthoring.secret.inbox.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
295 .services.CoAuthoring.secret.outbox.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
296 .services.CoAuthoring.secret.session.string = "'"$(cat ${cfg.jwtSecretFile})"'" |
297 ''}
298 .rabbitmq.url = "${cfg.rabbitmqUrl}"
299 ' /run/onlyoffice/config/default.json | sponge /run/onlyoffice/config/default.json
300
301 chmod u+w /run/onlyoffice/config/production-linux.json
302 jq '.FileConverter.converter.x2tPath = "${cfg.x2t}/bin/x2t"' \
303 /run/onlyoffice/config/production-linux.json | sponge /run/onlyoffice/config/production-linux.json
304
305 if psql -d onlyoffice -c "SELECT 'task_result'::regclass;" >/dev/null; then
306 psql -f ${cfg.package}/var/www/onlyoffice/documentserver/server/schema/postgresql/removetbl.sql
307 psql -f ${cfg.package}/var/www/onlyoffice/documentserver/server/schema/postgresql/createdb.sql
308 else
309 psql -f ${cfg.package}/var/www/onlyoffice/documentserver/server/schema/postgresql/createdb.sql
310 fi
311 '';
312 in
313 {
314 description = "onlyoffice documentserver";
315 after = [
316 "network.target"
317 "postgresql.service"
318 ];
319 requires = [ "postgresql.service" ];
320 wantedBy = [ "multi-user.target" ];
321 serviceConfig = {
322 ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper DocService/docservice /run/onlyoffice/config";
323 ExecStartPre = [ onlyoffice-prestart ];
324 Group = "onlyoffice";
325 Restart = "always";
326 RuntimeDirectory = "onlyoffice";
327 StateDirectory = "onlyoffice";
328 Type = "simple";
329 User = "onlyoffice";
330 };
331 };
332 };
333
334 users.users = {
335 onlyoffice = {
336 description = "OnlyOffice Service";
337 group = "onlyoffice";
338 isSystemUser = true;
339 };
340
341 nginx.extraGroups = [ "onlyoffice" ];
342 };
343
344 users.groups.onlyoffice = { };
345 };
346}