at master 16 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9 10let 11 12 inherit (pkgs) 13 cups-pk-helper 14 libcupsfilters 15 cups-filters 16 xdg-utils 17 ; 18 19 cfg = config.services.printing; 20 cups = cfg.package; 21 22 polkitEnabled = config.security.polkit.enable; 23 24 additionalBackends = 25 pkgs.runCommand "additional-cups-backends" 26 { 27 preferLocalBuild = true; 28 } 29 '' 30 mkdir -p $out 31 if [ ! -e ${cups.out}/lib/cups/backend/smb ]; then 32 mkdir -p $out/lib/cups/backend 33 ln -sv ${pkgs.samba}/bin/smbspool $out/lib/cups/backend/smb 34 fi 35 36 # Provide support for printing via HTTPS. 37 if [ ! -e ${cups.out}/lib/cups/backend/https ]; then 38 mkdir -p $out/lib/cups/backend 39 ln -sv ${cups.out}/lib/cups/backend/ipp $out/lib/cups/backend/https 40 fi 41 ''; 42 43 # Here we can enable additional backends, filters, etc. that are not 44 # part of CUPS itself, e.g. the SMB backend is part of Samba. Since 45 # we can't update ${cups.out}/lib/cups itself, we create a symlink tree 46 # here and add the additional programs. The ServerBin directive in 47 # cups-files.conf tells cupsd to use this tree. 48 bindir = pkgs.buildEnv { 49 name = "cups-progs"; 50 paths = [ 51 cups.out 52 additionalBackends 53 libcupsfilters 54 cups-filters 55 pkgs.ghostscript 56 ] 57 ++ cfg.drivers; 58 pathsToLink = [ 59 "/lib" 60 "/share/cups" 61 "/bin" 62 ]; 63 postBuild = cfg.bindirCmds; 64 ignoreCollisions = true; 65 }; 66 67 writeConf = 68 name: text: 69 pkgs.writeTextFile { 70 inherit name text; 71 destination = "/etc/cups/${name}"; 72 }; 73 74 cupsFilesFile = writeConf "cups-files.conf" '' 75 SystemGroup root wheel lpadmin 76 77 ServerBin ${bindir}/lib/cups 78 DataDir ${bindir}/share/cups 79 DocumentRoot ${cups.out}/share/doc/cups 80 81 AccessLog syslog 82 ErrorLog syslog 83 PageLog syslog 84 85 TempDir ${cfg.tempDir} 86 87 SetEnv PATH /var/lib/cups/path/lib/cups/filter:/var/lib/cups/path/bin 88 89 # User and group used to run external programs, including 90 # those that actually send the job to the printer. Note that 91 # Udev sets the group of printer devices to `lp', so we want 92 # these programs to run as `lp' as well. 93 User cups 94 Group lp 95 96 ${cfg.extraFilesConf} 97 ''; 98 99 cupsdFile = writeConf "cupsd.conf" '' 100 ${concatMapStrings (addr: '' 101 Listen ${addr} 102 '') cfg.listenAddresses} 103 Listen /run/cups/cups.sock 104 105 DefaultShared ${if cfg.defaultShared then "Yes" else "No"} 106 107 Browsing ${if cfg.browsing then "Yes" else "No"} 108 109 WebInterface ${if cfg.webInterface then "Yes" else "No"} 110 111 LogLevel ${cfg.logLevel} 112 113 ${cfg.extraConf} 114 ''; 115 116 browsedFile = writeConf "cups-browsed.conf" cfg.browsedConf; 117 118 rootdir = pkgs.buildEnv { 119 name = "cups-progs"; 120 paths = [ 121 cupsFilesFile 122 cupsdFile 123 (writeConf "client.conf" cfg.clientConf) 124 (writeConf "snmp.conf" cfg.snmpConf) 125 ] 126 ++ optional cfg.browsed.enable browsedFile 127 ++ cfg.drivers; 128 pathsToLink = [ "/etc/cups" ]; 129 ignoreCollisions = true; 130 }; 131 132 filterGutenprint = filter (pkg: pkg.meta.isGutenprint or false == true); 133 containsGutenprint = pkgs: length (filterGutenprint pkgs) > 0; 134 getGutenprint = pkgs: head (filterGutenprint pkgs); 135 136 parsePorts = 137 addresses: 138 let 139 splitAddress = addr: strings.splitString ":" addr; 140 extractPort = addr: builtins.foldl' (a: b: b) "" (splitAddress addr); 141 in 142 builtins.map (address: strings.toInt (extractPort address)) addresses; 143 144in 145 146{ 147 148 imports = [ 149 (mkChangedOptionModule [ "services" "printing" "gutenprint" ] [ "services" "printing" "drivers" ] ( 150 config: 151 let 152 enabled = getAttrFromPath [ "services" "printing" "gutenprint" ] config; 153 in 154 if enabled then [ pkgs.gutenprint ] else [ ] 155 )) 156 (mkRemovedOptionModule [ "services" "printing" "cupsFilesConf" ] "") 157 (mkRemovedOptionModule [ "services" "printing" "cupsdConf" ] "") 158 ]; 159 160 ###### interface 161 162 options = { 163 services.printing = { 164 165 enable = mkOption { 166 type = types.bool; 167 default = false; 168 description = '' 169 Whether to enable printing support through the CUPS daemon. 170 ''; 171 }; 172 173 package = lib.mkPackageOption pkgs "cups" { }; 174 175 stateless = mkOption { 176 type = types.bool; 177 default = false; 178 description = '' 179 If set, all state directories relating to CUPS will be removed on 180 startup of the service. 181 ''; 182 }; 183 184 startWhenNeeded = mkOption { 185 type = types.bool; 186 default = true; 187 description = '' 188 If set, CUPS is socket-activated; that is, 189 instead of having it permanently running as a daemon, 190 systemd will start it on the first incoming connection. 191 ''; 192 }; 193 194 listenAddresses = mkOption { 195 type = types.listOf types.str; 196 default = [ "localhost:631" ]; 197 example = [ "*:631" ]; 198 description = '' 199 A list of addresses and ports on which to listen. 200 ''; 201 }; 202 203 allowFrom = mkOption { 204 type = types.listOf types.str; 205 default = [ "localhost" ]; 206 example = [ "all" ]; 207 apply = concatMapStringsSep "\n" (x: "Allow ${x}"); 208 description = '' 209 From which hosts to allow unconditional access. 210 ''; 211 }; 212 213 openFirewall = mkOption { 214 type = types.bool; 215 default = false; 216 description = '' 217 Whether to open the firewall for TCP ports specified in 218 listenAddresses option. 219 ''; 220 }; 221 222 bindirCmds = mkOption { 223 type = types.lines; 224 internal = true; 225 default = ""; 226 description = '' 227 Additional commands executed while creating the directory 228 containing the CUPS server binaries. 229 ''; 230 }; 231 232 defaultShared = mkOption { 233 type = types.bool; 234 default = false; 235 description = '' 236 Specifies whether local printers are shared by default. 237 ''; 238 }; 239 240 browsing = mkOption { 241 type = types.bool; 242 default = false; 243 description = '' 244 Specifies whether shared printers are advertised. 245 ''; 246 }; 247 248 webInterface = mkOption { 249 type = types.bool; 250 default = true; 251 description = '' 252 Specifies whether the web interface is enabled. 253 ''; 254 }; 255 256 logLevel = mkOption { 257 type = types.str; 258 default = "info"; 259 example = "debug"; 260 description = '' 261 Specifies the cupsd logging verbosity. 262 ''; 263 }; 264 265 extraFilesConf = mkOption { 266 type = types.lines; 267 default = ""; 268 description = '' 269 Extra contents of the configuration file of the CUPS daemon 270 ({file}`cups-files.conf`). 271 ''; 272 }; 273 274 extraConf = mkOption { 275 type = types.lines; 276 default = ""; 277 example = '' 278 BrowsePoll cups.example.com 279 MaxCopies 42 280 ''; 281 description = '' 282 Extra contents of the configuration file of the CUPS daemon 283 ({file}`cupsd.conf`). 284 ''; 285 }; 286 287 clientConf = mkOption { 288 type = types.lines; 289 default = ""; 290 example = '' 291 ServerName server.example.com 292 Encryption Never 293 ''; 294 description = '' 295 The contents of the client configuration. 296 ({file}`client.conf`) 297 ''; 298 }; 299 300 browsed.enable = mkOption { 301 type = types.bool; 302 default = config.services.avahi.enable; 303 defaultText = literalExpression "config.services.avahi.enable"; 304 description = '' 305 Whether to enable the CUPS Remote Printer Discovery (browsed) daemon. 306 ''; 307 }; 308 309 browsed.package = lib.mkPackageOption pkgs "cups-browsed" { }; 310 311 browsedConf = mkOption { 312 type = types.lines; 313 default = ""; 314 example = '' 315 BrowsePoll cups.example.com 316 ''; 317 description = '' 318 The contents of the configuration. file of the CUPS Browsed daemon 319 ({file}`cups-browsed.conf`) 320 ''; 321 }; 322 323 snmpConf = mkOption { 324 type = types.lines; 325 default = '' 326 Address @LOCAL 327 ''; 328 description = '' 329 The contents of {file}`/etc/cups/snmp.conf`. See "man 330 cups-snmp.conf" for a complete description. 331 ''; 332 }; 333 334 drivers = mkOption { 335 type = types.listOf types.path; 336 default = [ ]; 337 example = literalExpression "with pkgs; [ gutenprint hplip splix ]"; 338 description = '' 339 CUPS drivers to use. Drivers provided by CUPS, cups-filters, 340 Ghostscript and Samba are added unconditionally. If this list contains 341 Gutenprint (i.e. a derivation with 342 `meta.isGutenprint = true`) the PPD files in 343 {file}`/var/lib/cups/ppd` will be updated automatically 344 to avoid errors due to incompatible versions. 345 ''; 346 }; 347 348 tempDir = mkOption { 349 type = types.path; 350 default = "/tmp"; 351 example = "/tmp/cups"; 352 description = '' 353 CUPSd temporary directory. 354 ''; 355 }; 356 }; 357 358 }; 359 360 ###### implementation 361 362 config = mkIf config.services.printing.enable { 363 364 users = { 365 users.cups = { 366 uid = config.ids.uids.cups; 367 group = "lp"; 368 description = "CUPS printing services"; 369 }; 370 371 # It seems that groups provided for `SystemGroup` must exist 372 groups.lpadmin = { }; 373 }; 374 375 # We need xdg-open (part of xdg-utils) for the desktop-file to proper open the users default-browser when opening "Manage Printing" 376 # https://github.com/NixOS/nixpkgs/pull/237994#issuecomment-1597510969 377 environment.systemPackages = [ 378 cups.out 379 xdg-utils 380 ] 381 ++ optional polkitEnabled cups-pk-helper; 382 environment.etc.cups.source = "/var/lib/cups"; 383 384 services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper; 385 services.udev.packages = cfg.drivers; 386 387 # Allow passwordless printer admin for members of wheel group 388 security.polkit.extraConfig = mkIf polkitEnabled '' 389 polkit.addRule(function(action, subject) { 390 if (action.id == "org.opensuse.cupspkhelper.mechanism.all-edit" && 391 subject.isInGroup("wheel")){ 392 return polkit.Result.YES; 393 } 394 }); 395 ''; 396 397 # Cups uses libusb to talk to printers, and does not use the 398 # linux kernel driver. If the driver is not in a black list, it 399 # gets loaded, and then cups cannot access the printers. 400 boot.blacklistedKernelModules = [ "usblp" ]; 401 402 # Some programs like print-manager rely on this value to get 403 # printer test pages. 404 environment.sessionVariables.CUPS_DATADIR = "${bindir}/share/cups"; 405 406 systemd.packages = [ cups.out ]; 407 408 systemd.sockets.cups = mkIf cfg.startWhenNeeded { 409 wantedBy = [ "sockets.target" ]; 410 listenStreams = [ 411 "" 412 "/run/cups/cups.sock" 413 ] 414 ++ map ( 415 x: replaceStrings [ "localhost" ] [ "127.0.0.1" ] (removePrefix "*:" x) 416 ) cfg.listenAddresses; 417 }; 418 419 systemd.services.cups = { 420 wantedBy = optionals (!cfg.startWhenNeeded) [ "multi-user.target" ]; 421 wants = [ "network.target" ]; 422 after = [ "network.target" ]; 423 424 path = [ cups.out ]; 425 426 preStart = 427 lib.optionalString cfg.stateless '' 428 rm -rf /var/cache/cups /var/lib/cups /var/spool/cups 429 '' 430 + '' 431 (umask 022 && mkdir -p /var/cache /var/lib /var/spool) 432 (umask 077 && mkdir -p /var/cache/cups /var/spool/cups) 433 (umask 022 && mkdir -p ${cfg.tempDir} /var/lib/cups) 434 # While cups will automatically create self-signed certificates if accessed via TLS, 435 # this directory to store the certificates needs to be created manually. 436 (umask 077 && mkdir -p /var/lib/cups/ssl) 437 438 # Backwards compatibility 439 if [ ! -L /etc/cups ]; then 440 mv /etc/cups/* /var/lib/cups 441 rmdir /etc/cups 442 ln -s /var/lib/cups /etc/cups 443 fi 444 # First, clean existing symlinks 445 if [ -n "$(ls /var/lib/cups)" ]; then 446 for i in /var/lib/cups/*; do 447 [ -L "$i" ] && rm "$i" 448 done 449 fi 450 # Then, populate it with static files 451 cd ${rootdir}/etc/cups 452 for i in *; do 453 [ ! -e "/var/lib/cups/$i" ] && ln -s "${rootdir}/etc/cups/$i" "/var/lib/cups/$i" 454 done 455 456 #update path reference 457 [ -L /var/lib/cups/path ] && \ 458 rm /var/lib/cups/path 459 [ ! -e /var/lib/cups/path ] && \ 460 ln -s ${bindir} /var/lib/cups/path 461 462 ${optionalString (containsGutenprint cfg.drivers) '' 463 if [ -d /var/lib/cups/ppd ]; then 464 ${getGutenprint cfg.drivers}/bin/cups-genppdupdate -x -p /var/lib/cups/ppd 465 fi 466 ''} 467 ''; 468 469 serviceConfig.PrivateTmp = true; 470 }; 471 472 systemd.services.cups-browsed = mkIf cfg.browsed.enable { 473 description = "CUPS Remote Printer Discovery"; 474 475 wantedBy = [ "multi-user.target" ]; 476 wants = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 477 bindsTo = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 478 partOf = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 479 after = [ "avahi-daemon.service" ] ++ optional (!cfg.startWhenNeeded) "cups.service"; 480 481 path = [ cups ]; 482 483 serviceConfig.ExecStart = "${cfg.browsed.package}/bin/cups-browsed"; 484 485 restartTriggers = [ browsedFile ]; 486 }; 487 488 services.printing.extraConf = '' 489 DefaultAuthType Basic 490 491 <Location /> 492 Order allow,deny 493 ${cfg.allowFrom} 494 </Location> 495 496 <Location /admin> 497 Order allow,deny 498 ${cfg.allowFrom} 499 </Location> 500 501 <Location /admin/conf> 502 AuthType Basic 503 Require user @SYSTEM 504 Order allow,deny 505 ${cfg.allowFrom} 506 </Location> 507 508 <Policy default> 509 <Limit Send-Document Send-URI Hold-Job Release-Job Restart-Job Purge-Jobs Set-Job-Attributes Create-Job-Subscription Renew-Subscription Cancel-Subscription Get-Notifications Reprocess-Job Cancel-Current-Job Suspend-Current-Job Resume-Job CUPS-Move-Job> 510 Require user @OWNER @SYSTEM 511 Order deny,allow 512 </Limit> 513 514 <Limit Pause-Printer Resume-Printer Set-Printer-Attributes Enable-Printer Disable-Printer Pause-Printer-After-Current-Job Hold-New-Jobs Release-Held-New-Jobs Deactivate-Printer Activate-Printer Restart-Printer Shutdown-Printer Startup-Printer Promote-Job Schedule-Job-After CUPS-Add-Printer CUPS-Delete-Printer CUPS-Add-Class CUPS-Delete-Class CUPS-Accept-Jobs CUPS-Reject-Jobs CUPS-Set-Default> 515 AuthType Basic 516 Require user @SYSTEM 517 Order deny,allow 518 </Limit> 519 520 <Limit Cancel-Job CUPS-Authenticate-Job> 521 Require user @OWNER @SYSTEM 522 Order deny,allow 523 </Limit> 524 525 <Limit All> 526 Order deny,allow 527 </Limit> 528 </Policy> 529 ''; 530 531 security.pam.services.cups = { }; 532 533 networking.firewall = 534 let 535 listenPorts = parsePorts cfg.listenAddresses; 536 in 537 mkIf cfg.openFirewall { 538 allowedTCPPorts = listenPorts; 539 }; 540 541 }; 542 543 meta.maintainers = with lib.maintainers; [ matthewbauer ]; 544 545}