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