at master 12 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.gotenberg; 9 10 args = [ 11 "--api-port=${toString cfg.port}" 12 "--api-timeout=${cfg.timeout}" 13 "--api-root-path=${cfg.rootPath}" 14 "--log-level=${cfg.logLevel}" 15 "--chromium-max-queue-size=${toString cfg.chromium.maxQueueSize}" 16 "--libreoffice-restart-after=${toString cfg.libreoffice.restartAfter}" 17 "--libreoffice-max-queue-size=${toString cfg.libreoffice.maxQueueSize}" 18 "--pdfengines-merge-engines=${lib.concatStringsSep "," cfg.pdfEngines.merge}" 19 "--pdfengines-convert-engines=${lib.concatStringsSep "," cfg.pdfEngines.convert}" 20 "--pdfengines-read-metadata-engines=${lib.concatStringsSep "," cfg.pdfEngines.readMetadata}" 21 "--pdfengines-write-metadata-engines=${lib.concatStringsSep "," cfg.pdfEngines.writeMetadata}" 22 "--api-download-from-allow-list=${cfg.downloadFrom.allowList}" 23 "--api-download-from-max-retry=${toString cfg.downloadFrom.maxRetries}" 24 ] 25 ++ optional cfg.enableBasicAuth "--api-enable-basic-auth" 26 ++ optional cfg.chromium.autoStart "--chromium-auto-start" 27 ++ optional cfg.chromium.disableJavascript "--chromium-disable-javascript" 28 ++ optional cfg.chromium.disableRoutes "--chromium-disable-routes" 29 ++ optional cfg.libreoffice.autoStart "--libreoffice-auto-start" 30 ++ optional cfg.libreoffice.disableRoutes "--libreoffice-disable-routes" 31 ++ optional cfg.pdfEngines.disableRoutes "--pdfengines-disable-routes" 32 ++ optional ( 33 cfg.downloadFrom.denyList != null 34 ) "--api-download-from-deny-list=${cfg.downloadFrom.denyList}" 35 ++ optional cfg.downloadFrom.disable "--api-disable-download-from" 36 ++ optional (cfg.bodyLimit != null) "--api-body-limit=${cfg.bodyLimit}" 37 ++ lib.optionals (cfg.extraArgs != [ ]) cfg.extraArgs; 38 39 inherit (lib) 40 mkEnableOption 41 mkPackageOption 42 mkOption 43 types 44 mkIf 45 optional 46 optionalAttrs 47 ; 48in 49{ 50 options = { 51 services.gotenberg = { 52 enable = mkEnableOption "Gotenberg, a stateless API for PDF files"; 53 54 # Users can override only gotenberg, libreoffice and chromium if they want to (eg. ungoogled-chromium, different LO version, etc) 55 # Don't allow setting the qpdf, pdftk, or unoconv paths, as those are very stable 56 # and there's only one version of each. 57 package = mkPackageOption pkgs "gotenberg" { }; 58 59 port = mkOption { 60 type = types.port; 61 default = 3000; 62 description = "Port on which the API should listen."; 63 }; 64 65 bindIP = mkOption { 66 type = types.nullOr types.str; 67 default = "127.0.0.1"; 68 description = "Port the API listener should bind to. Set to 0.0.0.0 to listen on all available IPs."; 69 }; 70 71 timeout = mkOption { 72 type = types.nullOr types.str; 73 default = "30s"; 74 description = "Timeout for API requests."; 75 }; 76 77 rootPath = mkOption { 78 type = types.str; 79 default = "/"; 80 description = "Root path for the Gotenberg API."; 81 }; 82 83 enableBasicAuth = mkOption { 84 type = types.bool; 85 default = false; 86 description = '' 87 HTTP Basic Authentication. 88 89 If you set this, be sure to set `GOTENBERG_API_BASIC_AUTH_USERNAME`and `GOTENBERG_API_BASIC_AUTH_PASSWORD` 90 in your `services.gotenberg.environmentFile` file. 91 ''; 92 }; 93 94 bodyLimit = mkOption { 95 type = types.nullOr types.str; 96 default = null; 97 description = "Sets the max limit for `multipart/form-data` requests. Accepts values like '5M', '20G', etc."; 98 }; 99 100 extraFontPackages = mkOption { 101 type = types.listOf types.package; 102 default = [ ]; 103 description = "Extra fonts to make available."; 104 }; 105 106 chromium = { 107 package = mkPackageOption pkgs "chromium" { }; 108 109 maxQueueSize = mkOption { 110 type = types.ints.unsigned; 111 default = 0; 112 description = "Maximum queue size for chromium-based conversions. Setting to 0 disables the limit."; 113 }; 114 115 autoStart = mkOption { 116 type = types.bool; 117 default = false; 118 description = "Automatically start Chromium when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it."; 119 }; 120 121 disableJavascript = mkOption { 122 type = types.bool; 123 default = false; 124 description = "Disable Javascript execution."; 125 }; 126 127 disableRoutes = mkOption { 128 type = types.bool; 129 default = false; 130 description = "Disable all routes allowing Chromium-based conversion."; 131 }; 132 }; 133 134 downloadFrom = { 135 allowList = mkOption { 136 type = types.nullOr types.str; 137 default = ".*"; 138 description = "Allow these URLs to be used in the `downloadFrom` API field. Accepts a regular expression."; 139 }; 140 denyList = mkOption { 141 type = types.nullOr types.str; 142 default = null; 143 description = "Deny accepting URLs from these domains in the `downloadFrom` API field. Accepts a regular expression."; 144 }; 145 maxRetries = mkOption { 146 type = types.ints.unsigned; 147 default = 4; 148 description = "The maximum amount of times to retry downloading a file specified with `downloadFrom`."; 149 }; 150 disable = mkOption { 151 type = types.bool; 152 default = false; 153 description = "Whether to disable the ability to download files for conversion from outside sources."; 154 }; 155 }; 156 157 libreoffice = { 158 package = mkPackageOption pkgs "libreoffice" { }; 159 160 restartAfter = mkOption { 161 type = types.ints.unsigned; 162 default = 10; 163 description = "Restart LibreOffice after this many conversions. Setting to 0 disables this feature."; 164 }; 165 166 maxQueueSize = mkOption { 167 type = types.ints.unsigned; 168 default = 0; 169 description = "Maximum queue size for LibreOffice-based conversions. Setting to 0 disables the limit."; 170 }; 171 172 autoStart = mkOption { 173 type = types.bool; 174 default = false; 175 description = "Automatically start LibreOffice when Gotenberg starts. If false, LibreOffice will start on the first conversion request that uses it."; 176 }; 177 178 disableRoutes = mkOption { 179 type = types.bool; 180 default = false; 181 description = "Disable all routes allowing LibreOffice-based conversion."; 182 }; 183 }; 184 185 pdfEngines = { 186 merge = mkOption { 187 type = types.listOf ( 188 types.enum [ 189 "qpdf" 190 "pdfcpu" 191 "pdftk" 192 ] 193 ); 194 default = [ 195 "qpdf" 196 "pdfcpu" 197 "pdftk" 198 ]; 199 description = "PDF Engines to use for merging files."; 200 }; 201 convert = mkOption { 202 type = types.listOf ( 203 types.enum [ 204 "libreoffice-pdfengine" 205 ] 206 ); 207 default = [ 208 "libreoffice-pdfengine" 209 ]; 210 description = "PDF Engines to use for converting files."; 211 }; 212 readMetadata = mkOption { 213 type = types.listOf ( 214 types.enum [ 215 "exiftool" 216 ] 217 ); 218 default = [ 219 "exiftool" 220 ]; 221 description = "PDF Engines to use for reading metadata from files."; 222 }; 223 writeMetadata = mkOption { 224 type = types.listOf ( 225 types.enum [ 226 "exiftool" 227 ] 228 ); 229 default = [ 230 "exiftool" 231 ]; 232 description = "PDF Engines to use for writing metadata to files."; 233 }; 234 235 disableRoutes = mkOption { 236 type = types.bool; 237 default = false; 238 description = "Disable routes related to PDF engines."; 239 }; 240 }; 241 242 logLevel = mkOption { 243 type = types.enum [ 244 "error" 245 "warn" 246 "info" 247 "debug" 248 ]; 249 default = "info"; 250 description = "The logging level for Gotenberg."; 251 }; 252 253 environmentFile = mkOption { 254 type = types.nullOr types.path; 255 default = null; 256 description = "Environment file to load extra environment variables from."; 257 }; 258 259 extraArgs = mkOption { 260 type = types.listOf types.str; 261 default = [ ]; 262 description = "Any extra command-line flags to pass to the Gotenberg service."; 263 }; 264 }; 265 }; 266 267 config = mkIf cfg.enable { 268 assertions = [ 269 { 270 assertion = cfg.enableBasicAuth -> cfg.environmentFile != null; 271 message = '' 272 When enabling HTTP Basic Authentication with `services.gotenberg.enableBasicAuth`, 273 you must provide an environment file via `services.gotenberg.environmentFile` with the appropriate environment variables set in it. 274 275 See `services.gotenberg.enableBasicAuth` for the names of those variables. 276 ''; 277 } 278 { 279 assertion = !(lib.isList cfg.pdfEngines); 280 message = '' 281 Setting `services.gotenberg.pdfEngines` to a list is now deprecated. 282 Use the new `pdfEngines.mergeEngines`, `pdfEngines.convertEngines`, `pdfEngines.readMetadataEngines`, and `pdfEngines.writeMetadataEngines` settings instead. 283 284 The previous option was using a method that is now deprecated by upstream. 285 ''; 286 } 287 ]; 288 289 systemd.services.gotenberg = { 290 description = "Gotenberg API server"; 291 after = [ "network.target" ]; 292 wantedBy = [ "multi-user.target" ]; 293 path = [ cfg.package ]; 294 environment = { 295 LIBREOFFICE_BIN_PATH = "${cfg.libreoffice.package}/lib/libreoffice/program/soffice.bin"; 296 CHROMIUM_BIN_PATH = lib.getExe cfg.chromium.package; 297 FONTCONFIG_FILE = pkgs.makeFontsConf { 298 fontDirectories = [ pkgs.liberation_ttf_v2 ] ++ cfg.extraFontPackages; 299 }; 300 # Needed for LibreOffice to work correctly. 301 # https://github.com/NixOS/nixpkgs/issues/349123#issuecomment-2418330936 302 HOME = "/run/gotenberg"; 303 }; 304 serviceConfig = { 305 Type = "simple"; 306 # NOTE: disable to debug chromium crashes or otherwise no coredump is created and forbidden syscalls are not being logged 307 DynamicUser = true; 308 ExecStart = "${lib.getExe cfg.package} ${lib.escapeShellArgs args}"; 309 310 # Needed for LibreOffice to work correctly. 311 # See above issue comment. 312 WorkingDirectory = "/run/gotenberg"; 313 RuntimeDirectory = "gotenberg"; 314 315 # Hardening options 316 PrivateDevices = true; 317 PrivateIPC = true; 318 PrivateUsers = true; 319 320 ProtectClock = true; 321 ProtectControlGroups = true; 322 ProtectHome = true; 323 ProtectHostname = true; 324 ProtectKernelLogs = true; 325 ProtectKernelModules = true; 326 ProtectKernelTunables = true; 327 ProtectProc = "invisible"; 328 329 RestrictAddressFamilies = [ 330 "AF_UNIX" 331 "AF_INET" 332 "AF_INET6" 333 "AF_NETLINK" 334 ]; 335 RestrictNamespaces = true; 336 RestrictRealtime = true; 337 338 LockPersonality = true; 339 340 SystemCallFilter = [ 341 "@sandbox" 342 "@system-service" 343 "@chown" 344 "@pkey" # required by chromium or it crashes 345 "mincore" 346 ]; 347 SystemCallArchitectures = "native"; 348 349 UMask = 77; 350 } 351 // optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; }; 352 }; 353 }; 354 355 meta.maintainers = with lib.maintainers; [ pyrox0 ]; 356}