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