From 56bc902b236bac6db8211ac8c48e441a82d84feb Mon Sep 17 00:00:00 2001 From: Yarny0 <41838844+Yarny0@users.noreply.github.com> Date: Tue, 19 Jul 2022 19:07:36 +0200 Subject: [PATCH 1/4] cups-pdf-to-pdf: init at unstable-2021-12-22 Note that cups-pdf refuses to run without root privileges. To use the binary, one has to either convince cups to call it with root privileges, or install it suid root. Also note that currently, this cups-pdf-fork produces small pdfs with selectable text, as promised. However, copying the text produces "garbled" text (characters are randomly reassigned). This is a known issue and I don't know how to fix it: https://github.com/alexivkin/CUPS-PDF-to-PDF/issues/7 --- .../cups/drivers/cups-pdf-to-pdf/default.nix | 59 +++++++++++++++++++ pkgs/top-level/all-packages.nix | 2 + 2 files changed, 61 insertions(+) create mode 100644 pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix diff --git a/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix b/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix new file mode 100644 index 000000000000..f85925ea956d --- /dev/null +++ b/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix @@ -0,0 +1,59 @@ +{ lib +, stdenv +, fetchFromGitHub +, cups +, coreutils +}: + +stdenv.mkDerivation rec { + pname = "cups-pdf-to-pdf"; + version = "unstable-2021-12-22"; + + src = fetchFromGitHub { + owner = "alexivkin"; + repo = "CUPS-PDF-to-PDF"; + rev = "c14428c2ca8e95371daad7db6d11c84046b1a2d4"; + hash = "sha256-pa4PFf8OAFSra0hSazmKUfbMYL/cVWvYA1lBf7c7jmY="; + }; + + buildInputs = [ cups ]; + + postPatch = '' + sed -r 's|(gscall, size, ")cp |\1${coreutils}/bin/cp |' cups-pdf.c -i + ''; + + # gcc command line is taken from original cups-pdf's README file + # https://fossies.org/linux/cups-pdf/README + # however, we replace gcc with $CC following + # https://nixos.org/manual/nixpkgs/stable/#sec-darwin + buildPhase = '' + runHook preBuild + $CC -O9 -s cups-pdf.c -o cups-pdf -lcups + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + install -Dt $out/lib/cups/backend cups-pdf + install -Dm 0644 -t $out/etc/cups cups-pdf.conf + install -Dm 0644 -t $out/share/cups/model *.ppd + runHook postInstall + ''; + + meta = with lib; { + description = "A CUPS backend that turns print jobs into searchable PDF files"; + homepage = "https://github.com/alexivkin/CUPS-PDF-to-PDF"; + license = licenses.gpl2Only; + maintainers = [ maintainers.yarny ]; + longDescription = '' + cups-pdf is a CUPS backend that generates a PDF file for each print job and puts this file + into a folder on the local machine such that the print job's owner can access the file. + + https://www.cups-pdf.de/ + + cups-pdf-to-pdf is a fork of cups-pdf which tries hard to preserve the original text of the print job by avoiding rasterization. + + Note that in order to use this package, you have to make sure that the cups-pdf program is called with root privileges. + ''; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index a5b66dd4bc25..d1ee6af2f670 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -36497,6 +36497,8 @@ with pkgs; cups-dymo = callPackage ../misc/cups/drivers/dymo {}; + cups-pdf-to-pdf = callPackage ../misc/cups/drivers/cups-pdf-to-pdf {}; + cups-toshiba-estudio = callPackage ../misc/cups/drivers/estudio {}; cups-zj-58 = callPackage ../misc/cups/drivers/zj-58 { }; From 49a129ab4027ceeee3d4f67898ff6ee8067daeac Mon Sep 17 00:00:00 2001 From: Yarny0 <41838844+Yarny0@users.noreply.github.com> Date: Tue, 19 Jul 2022 19:42:28 +0200 Subject: [PATCH 2/4] nixos/cups-pdf: init Some implementation notes: * cups-pdf, and cups-pdf-to-pdf, support multiple instances with differing configurations. This can be accomplished by creating multiple configuration files with names `cups-pdf-{instance-name}.conf`. The Nixos module supports this feature by providing the option `instances` which is an attrset mapping instance names to instance configurations. To simplify module usage, an instance `pdf` is created by default. * To use a cups-pdf instance, one also needs a cups queue that connects to the backend. The module does this automatically by default, using the `hardware.printers.ensurePrinters`. It uses one of the ppd files which is included in the cups-pdf package. If this isn't desired (e.g. because printer queues should be created by hand, or configured differently), the `installPrinter` option can be turned off (for each instance separately). * In our configuration, cups calls external programs using the `cups` account and the `lp` group. cups-pdf refuses to operate without root privileges, likely because it needs to change the ownership of it output pdf files so that (only) the print job's owner can access them. The module installs a suid root wrapper for the backend program that can only be called by the `lp` group. The cups-pdf package is replaced by a wrapper package which calls the suid root wrapper. So cups can call its backend programs as usual. --- nixos/modules/module-list.nix | 1 + nixos/modules/services/printing/cups-pdf.nix | 185 +++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 nixos/modules/services/printing/cups-pdf.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 24dd30e15750..56f3b98556b7 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1018,6 +1018,7 @@ ./services/networking/znc/default.nix ./services/printing/cupsd.nix ./services/printing/ipp-usb.nix + ./services/printing/cups-pdf.nix ./services/scheduling/atd.nix ./services/scheduling/cron.nix ./services/scheduling/fcron.nix diff --git a/nixos/modules/services/printing/cups-pdf.nix b/nixos/modules/services/printing/cups-pdf.nix new file mode 100644 index 000000000000..07f24367132f --- /dev/null +++ b/nixos/modules/services/printing/cups-pdf.nix @@ -0,0 +1,185 @@ +{ config, lib, pkgs, ... }: + +let + + # cups calls its backends as user `lp` (which is good!), + # but cups-pdf wants to be called as `root`, so it can change ownership of files. + # We add a suid wrapper and a wrapper script to trick cups into calling the suid wrapper. + # Note that a symlink to the suid wrapper alone wouldn't suffice, cups would complain + # > File "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-cups-progs/lib/cups/backend/cups-pdf" has insecure permissions (0104554/uid=0/gid=20) + + # wrapper script that redirects calls to the suid wrapper + cups-pdf-wrapper = pkgs.writeTextFile { + name = "${pkgs.cups-pdf-to-pdf.name}-wrapper.sh"; + executable = true; + destination = "/lib/cups/backend/cups-pdf"; + checkPhase = '' + ${pkgs.stdenv.shellDryRun} "$target" + ${lib.getExe pkgs.shellcheck} "$target" + ''; + text = '' + #! ${pkgs.runtimeShell} + exec "${config.security.wrapperDir}/cups-pdf" "$@" + ''; + }; + + # wrapped cups-pdf package that uses the suid wrapper + cups-pdf-wrapped = pkgs.buildEnv { + name = "${pkgs.cups-pdf-to-pdf.name}-wrapped"; + # using the wrapper as first path ensures it is used + paths = [ cups-pdf-wrapper pkgs.cups-pdf-to-pdf ]; + ignoreCollisions = true; + }; + + instanceSettings = name: { + freeformType = with lib.types; nullOr (oneOf [ int str path package ]); + # override defaults: + # inject instance name into paths, + # also avoid conflicts between user names and special dirs + options.Out = lib.mkOption { + type = with lib.types; nullOr singleLineStr; + default = "/var/spool/cups-pdf-${name}/users/\${USER}"; + defaultText = "/var/spool/cups-pdf-{instance-name}/users/\${USER}"; + example = "\${HOME}/cups-pdf"; + description = lib.mdDoc '' + output directory; + `''${HOME}` will be expanded to the user's home directory, + `''${USER}` will be expanded to the user name. + ''; + }; + options.AnonDirName = lib.mkOption { + type = with lib.types; nullOr singleLineStr; + default = "/var/spool/cups-pdf-${name}/anonymous"; + defaultText = "/var/spool/cups-pdf-{instance-name}/anonymous"; + example = "/var/lib/cups-pdf"; + description = lib.mdDoc "path for anonymously created PDF files"; + }; + options.Spool = lib.mkOption { + type = with lib.types; nullOr singleLineStr; + default = "/var/spool/cups-pdf-${name}/spool"; + defaultText = "/var/spool/cups-pdf-{instance-name}/spool"; + example = "/var/lib/cups-pdf"; + description = lib.mdDoc "spool directory"; + }; + options.Anonuser = lib.mkOption { + type = lib.types.singleLineStr; + default = "root"; + description = lib.mdDoc '' + User for anonymous PDF creation. + An empty string disables this feature. + ''; + }; + options.GhostScript = lib.mkOption { + type = with lib.types; nullOr path; + default = lib.getExe pkgs.ghostscript; + defaultText = lib.literalExpression "lib.getExe pkgs.ghostscript"; + example = lib.literalExpression ''''${pkgs.ghostscript}/bin/ps2pdf''; + description = lib.mdDoc "location of GhostScript binary"; + }; + }; + + instanceConfig = { name, config, ... }: { + options = { + enable = (lib.mkEnableOption (lib.mdDoc "this cups-pdf instance")) // { default = true; }; + installPrinter = (lib.mkEnableOption (lib.mdDoc '' + a CUPS printer queue for this instance. + The queue will be named after the instance and will use the {file}`CUPS-PDF_opt.ppd` ppd file. + If this is disabled, you need to add the queue yourself to use the instance + '')) // { default = true; }; + confFileText = lib.mkOption { + type = lib.types.lines; + description = lib.mdDoc '' + This will contain the contents of {file}`cups-pdf.conf` for this instance, derived from {option}`settings`. + You can use this option to append text to the file. + ''; + }; + settings = lib.mkOption { + type = lib.types.submodule (instanceSettings name); + default = {}; + example = { + Out = "\${HOME}/cups-pdf"; + UserUMask = "0033"; + }; + description = lib.mdDoc '' + Settings for a cups-pdf instance, see the descriptions in the template config file in the cups-pdf package. + The key value pairs declared here will be translated into proper key value pairs for {file}`cups-pdf.conf`. + Setting a value to `null` disables the option and removes it from the file. + ''; + }; + }; + config.confFileText = lib.pipe config.settings [ + (lib.filterAttrs (key: value: value != null)) + (lib.mapAttrs (key: builtins.toString)) + (lib.mapAttrsToList (key: value: "${key} ${value}\n")) + lib.concatStrings + ]; + }; + + cupsPdfCfg = config.services.printing.cups-pdf; + + copyConfigFileCmds = lib.pipe cupsPdfCfg.instances [ + (lib.filterAttrs (name: lib.getAttr "enable")) + (lib.mapAttrs (name: lib.getAttr "confFileText")) + (lib.mapAttrs (name: pkgs.writeText "cups-pdf-${name}.conf")) + (lib.mapAttrsToList (name: confFile: "ln --symbolic --no-target-directory ${confFile} /var/lib/cups/cups-pdf-${name}.conf\n")) + lib.concatStrings + ]; + + printerSettings = lib.pipe cupsPdfCfg.instances [ + (lib.filterAttrs (name: lib.getAttr "enable")) + (lib.filterAttrs (name: lib.getAttr "installPrinter")) + (lib.mapAttrsToList (name: instance: (lib.mapAttrs (key: lib.mkDefault) { + inherit name; + model = "CUPS-PDF_opt.ppd"; + deviceUri = "cups-pdf:/${name}"; + description = "virtual printer for cups-pdf instance ${name}"; + location = instance.settings.Out; + }))) + ]; + +in + +{ + + options.services.printing.cups-pdf = { + enable = lib.mkEnableOption (lib.mdDoc '' + the cups-pdf virtual pdf printer backend. + By default, this will install a single printer `pdf`. + but this can be changed/extended with {option}`services.printing.cups-pdf.instances` + ''); + instances = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule instanceConfig); + default.pdf = {}; + example.pdf.settings = { + Out = "\${HOME}/cups-pdf"; + UserUMask = "0033"; + }; + description = lib.mdDoc '' + Permits to raise one or more cups-pdf instances. + Each instance is named by an attribute name, and the attribute's values control the instance' configuration. + ''; + }; + }; + + config = lib.mkIf cupsPdfCfg.enable { + services.printing.enable = true; + services.printing.drivers = [ cups-pdf-wrapped ]; + hardware.printers.ensurePrinters = printerSettings; + # the cups module will install the default config file, + # but we don't need it and it would confuse cups-pdf + systemd.services.cups.preStart = lib.mkAfter '' + rm -f /var/lib/cups/cups-pdf.conf + ${copyConfigFileCmds} + ''; + security.wrappers.cups-pdf = { + group = "lp"; + owner = "root"; + permissions = "+r,ug+x"; + setuid = true; + source = "${pkgs.cups-pdf-to-pdf}/lib/cups/backend/cups-pdf"; + }; + }; + + meta.maintainers = [ lib.maintainers.yarny ]; + +} From 85aeeac28dea421c53cc4380e34750e8d23c2aaf Mon Sep 17 00:00:00 2001 From: Yarny0 <41838844+Yarny0@users.noreply.github.com> Date: Wed, 20 Jul 2022 20:14:06 +0200 Subject: [PATCH 3/4] cups-pdf: add vm test --- nixos/tests/all-tests.nix | 1 + nixos/tests/cups-pdf.nix | 40 +++++++++++++++++++ .../cups/drivers/cups-pdf-to-pdf/default.nix | 3 ++ 3 files changed, 44 insertions(+) create mode 100644 nixos/tests/cups-pdf.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 70cd995ececa..7604175bfdaf 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -151,6 +151,7 @@ in { coturn = handleTest ./coturn.nix {}; couchdb = handleTest ./couchdb.nix {}; cri-o = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cri-o.nix {}; + cups-pdf = handleTest ./cups-pdf.nix {}; custom-ca = handleTest ./custom-ca.nix {}; croc = handleTest ./croc.nix {}; deluge = handleTest ./deluge.nix {}; diff --git a/nixos/tests/cups-pdf.nix b/nixos/tests/cups-pdf.nix new file mode 100644 index 000000000000..70d14f29e2e5 --- /dev/null +++ b/nixos/tests/cups-pdf.nix @@ -0,0 +1,40 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: { + name = "cups-pdf"; + + nodes.machine = { pkgs, ... }: { + imports = [ ./common/user-account.nix ]; + environment.systemPackages = [ pkgs.poppler_utils ]; + fonts.fonts = [ pkgs.dejavu_fonts ]; # yields more OCR-able pdf + services.printing.cups-pdf.enable = true; + services.printing.cups-pdf.instances = { + opt = {}; + noopt.installPrinter = false; + }; + hardware.printers.ensurePrinters = [{ + name = "noopt"; + model = "CUPS-PDF_noopt.ppd"; + deviceUri = "cups-pdf:/noopt"; + }]; + }; + + # we cannot check the files with pdftotext, due to + # https://github.com/alexivkin/CUPS-PDF-to-PDF/issues/7 + # we need `imagemagickBig` as it has ghostscript support + + testScript = '' + from subprocess import run + machine.wait_for_unit("cups.service") + for name in ("opt", "noopt"): + text = f"test text {name}".upper() + machine.wait_until_succeeds(f"lpstat -v {name}") + machine.succeed(f"su - alice -c 'echo -e \"\n {text}\" | lp -d {name}'") + # wait until the pdf files are completely produced and readable by alice + machine.wait_until_succeeds(f"su - alice -c 'pdfinfo /var/spool/cups-pdf-{name}/users/alice/*.pdf'") + machine.succeed(f"cp /var/spool/cups-pdf-{name}/users/alice/*.pdf /tmp/{name}.pdf") + machine.copy_from_vm(f"/tmp/{name}.pdf", "") + run(f"${pkgs.imagemagickBig}/bin/convert -density 300 $out/{name}.pdf $out/{name}.jpeg", shell=True, check=True) + assert text.encode() in run(f"${lib.getExe pkgs.tesseract} $out/{name}.jpeg stdout", shell=True, check=True, capture_output=True).stdout + ''; + + meta.maintainers = [ lib.maintainers.yarny ]; +}) diff --git a/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix b/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix index f85925ea956d..a26216cbc727 100644 --- a/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix +++ b/pkgs/misc/cups/drivers/cups-pdf-to-pdf/default.nix @@ -3,6 +3,7 @@ , fetchFromGitHub , cups , coreutils +, nixosTests }: stdenv.mkDerivation rec { @@ -40,6 +41,8 @@ stdenv.mkDerivation rec { runHook postInstall ''; + passthru.tests.vmtest = nixosTests.cups-pdf; + meta = with lib; { description = "A CUPS backend that turns print jobs into searchable PDF files"; homepage = "https://github.com/alexivkin/CUPS-PDF-to-PDF"; From 3f11bdb2e7128e5dc0622b405f5095ba588d18de Mon Sep 17 00:00:00 2001 From: Yarny0 <41838844+Yarny0@users.noreply.github.com> Date: Thu, 21 Jul 2022 18:48:29 +0200 Subject: [PATCH 4/4] cups-pdf: mention new package/module in 23.05 release notes --- .../doc/manual/from_md/release-notes/rl-2305.section.xml | 9 +++++++++ nixos/doc/manual/release-notes/rl-2305.section.md | 2 ++ 2 files changed, 11 insertions(+) diff --git a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml index 61daeac86a64..12f19383e8e1 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml @@ -30,6 +30,15 @@ programs.bash.blesh. + + + cups-pdf-to-pdf, + a pdf-generating cups backend based on + cups-pdf. + Available as + services.printing.cups-pdf. + + fzf, diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index f9c76b02f891..53d3f21c6ab1 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -16,6 +16,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [blesh](https://github.com/akinomyoga/ble.sh), a line editor written in pure bash. Available as [programs.bash.blesh](#opt-programs.bash.blesh.enable). +- [cups-pdf-to-pdf](https://github.com/alexivkin/CUPS-PDF-to-PDF), a pdf-generating cups backend based on [cups-pdf](https://www.cups-pdf.de/). Available as [services.printing.cups-pdf](#opt-services.printing.cups-pdf.enable). + - [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion). ## Backward Incompatibilities {#sec-release-23.05-incompatibilities}