Show pagesourceOld revisionsBacklinksBack to top × Table of Contents Printing over SSH Introduction CUPS setup OpenWrt setup Non-CUPS usage Printing over SSH Introduction This page describes a way to make CUPS print, over SSH, to a USB printer attached to an OpenWrt device. No extra software is required, other than some shell scripts and the standard USB driver; and no storage is required on the OpenWrt device. CUPS setup Install the following script on a system running CUPS, using the instructions in its comment block. DeviceURI in printers.conf should point to your OpenWrt system. #!/bin/sh # copyright waived as per CC0: # https://creativecommons.org/publicdomain/zero/1.0/legalcode-plain # This CUPS backend will forward each job to the standard input of a program on # a remote system, via ssh. # # Install it as /usr/lib/cups/backend/ssh; either use "chmod u+x,go-wx" to make # it run as root, or "chmod ugo+x,go-w" to run as the CUPS user (generally lp). # Create a passwordless ssh key as that user, e.g.: # # mkdir -p -m 711 ~lp # # chown lp: ~lp # # su -s /bin/sh lp -c "ssh-keygen -N '' -t rsa -b 2048" # And add the key to an ssh authorized_keys file on the remote system: # command="./recv-lpjob",no-pty,no-port-forwarding ssh-rsa AAAAB2...19Q== lp@cups.example.net # # To enable it, add this to /etc/cups/printers.conf while cups is not running: # <Printer ssh_example> # UUID urn:uuid:e2f74bc0-2d45-4afd-915e-a16df20c49a1 # DeviceURI ssh://root@openwrt.example.net:22/dev/usb/lp0 # State Idle # Type 8388612 # Accepting Yes # Shared No # ErrorPolicy retry-current-job # </Printer> # Be sure to connect once manually so the host key fingerprint is known: # # su -s /bin/bash lp # $ ssh -p 22 root@openwrt.example.net < /dev/null # Install a PPD file as /etc/cups/ppd/ssh_example.ppd if necessary. # # To debug, set "LogLevel warn" in /etc/cups/cupsd.conf; stderr will be saved # in /var/log/cups/error_log. set -o errexit set -o nounset remote_cmd=./recv-lpjob CUPS_BACKEND_OK=0 CUPS_BACKEND_FAILED=1 CUPS_BACKEND_AUTH_REQUIRED=2 # cups will rewrite printers.conf with auth. data CUPS_BACKEND_HOLD=3 CUPS_BACKEND_STOP=4 CUPS_BACKEND_CANCEL=5 # could be bad options CUPS_BACKEND_RETRY=6 CUPS_BACKEND_RETRY_CURRENT=7 debugf () { dbg="[backend/ssh] $1" shift printf "$dbg" "$@" >&2 } if [ "$#" -eq 0 ] then debugf 'in discovery mode\n' # Output zero or more lines in any of these formats: # device-class scheme "Unknown" "device-info" # device-class device-uri "device-make-and-model" "device-info" # device-class device-uri "device-make-and-model" "device-info" "device-id" # device-class device-uri "device-make-and-model" "device-info" "device-id" "device-location" # Quoted strings use '\' as escape. printf 'direct ssh "Unknown" "%s"\n' "prints via ssh" exit "$CUPS_BACKEND_OK" elif [ "$#" -lt 5 ] then printf 'Usage: %s job-id user title copies options [file ...]\n' "$0" >&2 exit "$CUPS_BACKEND_FAILED" fi ssh_user='' ssh_host='' ssh_port='' ssh_path='' case "$DEVICE_URI" in ssh://*) # https://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04 x=${DEVICE_URI#ssh://} # strip off any path; the other end can try to handle it y=${x%%/*} ssh_path=${x#"$y"} x=$y case "$x" in *%*) debugf 'Unescaping not implemented for URI "%s"\n' \ "$DEVICE_URI" exit "$CUPS_BACKEND_CANCEL" esac case "$x" in *@*) ssh_user=${x%%@*} x=${x#*@} case "$ssh_user" in '' | *:* | *\;* | -*) debugf 'Bad/unsupported userinfo "%s" in URI "%s"\n' \ "$ssh_user" "$DEVICE_URI" exit "$CUPS_BACKEND_CANCEL" esac esac case "$x" in '' | [-:]* | *:*:*) debugf 'Bad address "%s" in URI "%s"\n' \ "$x" "$DEVICE_URI" exit "$CUPS_BACKEND_CANCEL" ;; *:*) ssh_port=${x#*:} x=${x%%:*} esac ssh_host=$x ;; *) debugf 'Bad scheme in URI "%s"\n' "$DEVICE_URI" exit "$CUPS_BACKEND_CANCEL" esac debugf 'ssh-user: %s\n' "$ssh_user" debugf 'ssh-host: %s\n' "$ssh_host" debugf 'ssh-port: %s\n' "$ssh_port" debugf 'ssh-path: %s\n' "$ssh_path" job_id=$1 user=$2 title=$3 copies=$4 options=$5 shift 5 # remaining arguments are filenames debugf 'job-id: %s\n' "$job_id" debugf 'user: %s\n' "$user" debugf 'title: %s\n' "$title" debugf 'copies: %s\n' "$copies" debugf 'options: %s\n' "$options" [ "$#" -gt 0 ] || debugf 'file not set\n' for file do debugf 'file: %s\n' "$file" done do_printing () { printf 'set path=%s\n' "$ssh_path" printf 'set job_id=%s\n' "$job_id" printf 'set user=%s\n' "$user" printf 'set title=%s\n' "$title" printf 'set options=%s\n' "$options" if [ "$#" -eq 0 ] then debugf 'printing stdin\n' printf 'print\n' cat if [ "$copies" -gt 1 ] then debugf 'cannot print multiple copies from stdin\n' fi else while [ "$copies" -gt 0 ] do copies=$((copies - 1)) n=$# while [ "$n" -gt 0 ] do n=$((n -= 1)) file=$1 shift set -- "$@" "$file" debugf 'printing file %s\n' "$file" printf 'print '; wc -c "$file" cat <"$file" done done fi debugf 'done printing\n' } do_ssh () { set -- ssh -o BatchMode=yes -o RequestTTY=no [ -z "$ssh_port" ] || set -- "$@" -p "$ssh_port" # use ~/.ssh/config (man ssh_config) to set global or per-host options if [ -n "$ssh_user" ] then set -- "$@" "$ssh_user@$ssh_host" else set -- "$@" "$ssh_host" fi set -- "$@" "$remote_cmd" debugf 'will run %s\n' "$*" "$@" >/dev/null && set -- 0 || set -- "$?" debugf 'ssh exited with status %d\n' "$1" printf '%d\n' "$1" } rc=$(do_printing "$@" | do_ssh) debugf 'exit status "%s"\n' "$rc" case "$rc" in '' | *[!0-9]*) exit "$CUPS_BACKEND_FAILED" ;; 127 | 255) # ssh error exit "$CUPS_BACKEND_RETRY_CURRENT" ;; esac exit "$rc" OpenWrt setup Save the following script as /root/recv-lpjob on your OpenWrt device: #!/bin/sh # copyright waived as per CC0: # https://creativecommons.org/publicdomain/zero/1.0/legalcode-plain set -o errexit set -o nounset CUPS_BACKEND_OK=0 CUPS_BACKEND_FAILED=1 CUPS_BACKEND_AUTH_REQUIRED=2 # cups will rewrite printers.conf with auth. data CUPS_BACKEND_HOLD=3 CUPS_BACKEND_STOP=4 CUPS_BACKEND_CANCEL=5 # could be bad options CUPS_BACKEND_RETRY=6 CUPS_BACKEND_RETRY_CURRENT=7 # state variables printer='' locked='' exec 3>&- # var_* variables controlled by sender var_path='' var_job_id='' var_user='' var_title='' var_options='' debugf () { dbg="[recv-lpjob] $1" shift printf "$dbg" "$@" >&2 } set_lock () { debugf 'setting locked=%d for "%s"\n' "$2" "$1" [ "$1" != /dev/null ] || return 0 if [ -x /bin/lock ] then debugf 'using /bin/lock\n' if [ "$2" -eq 0 ] then /bin/lock -u "$1.lock" || return else /bin/lock "$1.lock" || return fi else debugf 'no locks available\n' # it's OK to continue without locking; # Linux will fail a second open() with EBUSY fi } lock_and_open_printer () { if [ -n "$printer" ] then close_printer # unlock if we're switching printers [ "$1" = "$locked" ] || unlock_printer fi if [ -z "$locked" ] then if ! set_lock "$1" 1 then debugf 'failed to lock "%s"\n' "$1" return 1 fi if ! [ -c "$1" ] then debugf 'printer "%s" not found\n' "$1" return 1 fi locked=$1 fi if exec 3>"$1" then debugf 'opened printer "%s"\n' "$1" else debugf 'failed to open printer "%s"\n' "$1" fi printer=$1 } cat_to_printer () { cat >&3 } close_printer () { if [ -n "$printer" ] then debugf 'closing printer "%s"\n' "$printer" exec 3>&- printer='' fi } unlock_printer () { if [ -n "$locked" ] then set_lock "$locked" 0 locked='' fi } handle_set () { debugf 'got %s\n' "$1" case "$1" in *=*) key=${1%%=*} val=${1#"$key"} val=${val#=} ;; *) debugf 'bad set command "%s"\n' "$line" exit "$CUPS_BACKEND_CANCEL" esac case "$key" in '' | [0-9]* | *[!a-zA-Z_0-9]*) debugf 'bad key "%s"\n' "$key" exit "$CUPS_BACKEND_CANCEL" esac eval "var_$key=\$val" } write_exitcode () { "$@" >/dev/null && set -- 0 || set -- "$?" printf '%d\n' "$1" } handle_print () { case "$var_path" in '' | /) set -- /dev/usb/lp* if ! [ -e "$1" ] then debugf 'no default printer found\n' exit "$CUPS_BACKEND_RETRY_CURRENT" fi var_path=$1 ;; /dev/usb/lp[0-9]) ;; /dev/null) ;; *) debugf 'printer "%s" disallowed\n' "$var_path" exit "$CUPS_BACKEND_CANCEL" esac lock_and_open_printer "$var_path" || return arg=${1%% *} case "$arg" in '') debugf 'printing to eof\n' cat_to_printer ;; 0?* | *[!0-9]*) debugf 'bad print size "%s"\n' "$arg" exit "$CUPS_BACKEND_CANCEL" ;; *) arg=$((arg)) debugf 'printing %d bytes\n' "$arg" rc=$(head -c "$arg" | write_exitcode cat_to_printer) case "$rc" in '' | *[!0-9]*) exit "$CUPS_BACKEND_FAILED" esac [ "$rc" -eq 0 ] || exit "$rc" ;; esac debugf 'print command completed\n' } onexit () { debugf 'exiting\n' close_printer unlock_printer } trap onexit 0 IFS='' while read -r line do cmd=${line%% *} arg=${line#"$cmd"} arg=${arg# } case "$cmd" in set) handle_set "$arg";; print) handle_print "$arg";; *) debugf 'unknown protocol command "%s"\n' "$cmd" exit "$CUPS_BACKEND_CANCEL" esac done Make sure /etc/dropbear/authorized_keys will allow CUPS to connect (as described in the backend/ssh script); e.g., command="./recv-lpjob",no-pty,no-port-forwarding ssh-rsa AAAAB2...19Q== lp@cups.example.net Non-CUPS usage If not using CUPS, the backend can be called manually from the command line; e.g. (assuming the printer wants PJL): export DEVICE_URI=ssh://root@openwrt.example.net/dev/usb/lp0 /usr/lib/cups/backend/ssh 0 . '' 1 '' test.pjl If no filename is given, it will read from stdin. You can replace /dev/usb/lp0 with /dev/null (in the URI) for testing. To embed a raw PostScript file in PJL and send it: export DEVICE_URI=ssh://root@openwrt.example.net/dev/usb/lp0 ( printf '\33%%-12345X@PJL\n@PJL ENTER LANGUAGE = POSTSCRIPT\n' cat test.ps printf '\33%%-12345X' ) | /usr/lib/cups/backend/ssh 0 . '' 1 '' This website uses cookies. By using the website, you agree with storing cookies on your computer. Also you acknowledge that you have read and understand our Privacy Policy. If you do not agree leave the website.OKMore information about cookies Last modified: 2018/03/03 15:19by bobafetthotmail