This commit is contained in:
1 parent
7f26ca4f2f
commit
db23feada5
8 files changed
+561
-15
No files matched your search
+175
-5
@@ -1,9 +1,10 @@
|
||||
#!/bin/sh
|
||||
# Startup script for the containerized CUPS print server.
|
||||
#
|
||||
# Starts the D-Bus system bus, Avahi (mDNS/DNS-SD) and CUPS, then registers
|
||||
# the Brother HL-4150CDN queue if it does not exist yet. The CUPS daemon is
|
||||
# kept in the foreground so the container lives as long as the print server.
|
||||
# Starts the D-Bus system bus, Avahi (mDNS/DNS-SD) and CUPS, installs the TLS
|
||||
# certificate for the secure AirPrint service, then registers the Brother
|
||||
# HL-4150CDN queue if it does not exist yet. The CUPS daemon is kept in the
|
||||
# foreground so the container lives as long as the print server.
|
||||
#
|
||||
# The queue uses Brother's BR-Script3 (PostScript 3) PPD and, by default,
|
||||
# sends jobs to the printer over raw AppSocket (socket://<ip>:9100). CUPS
|
||||
@@ -17,6 +18,9 @@
|
||||
# PRINTER_NAME CUPS queue name and advertised (DNS-SD) name.
|
||||
# PRINTER_DESCRIPTION CUPS Description (lpadmin -D) shown in the web UI.
|
||||
# PPD_FILE PPD used for the queue.
|
||||
# CERT_EXTRA_NAMES Comma-separated extra DNS names / IP addresses to put
|
||||
# into the generated IPPS certificate (server SANs).
|
||||
# CERT_DAYS Validity of the generated certificate (default 3650).
|
||||
set -eu
|
||||
|
||||
PRINTER_NAME="${PRINTER_NAME:-HL4150CDN}"
|
||||
@@ -56,7 +60,11 @@ if [ ! -s /etc/machine-id ] && [ ! -s /var/lib/dbus/machine-id ]; then
|
||||
log "generating D-Bus machine id"
|
||||
dbus-uuidgen > /var/lib/dbus/machine-id
|
||||
fi
|
||||
if [ ! -S /run/dbus/system_bus_socket ]; then
|
||||
if ! pgrep -x dbus-daemon >/dev/null 2>&1; then
|
||||
# Check for the process, not the socket: a previous run leaves the socket and
|
||||
# PID file behind (podman restart, systemd Restart=), and dbus-daemon then
|
||||
# refuses to start and avahi-daemon would hang on a dead bus.
|
||||
rm -f /run/dbus/system_bus_socket /run/dbus/pid
|
||||
log "starting dbus-daemon (system bus)"
|
||||
dbus-daemon --system --fork
|
||||
fi
|
||||
@@ -65,9 +73,171 @@ fi
|
||||
mkdir -p /run/avahi-daemon
|
||||
if ! pgrep -x avahi-daemon >/dev/null 2>&1; then
|
||||
log "starting avahi-daemon"
|
||||
avahi-daemon --daemonize --no-chroot
|
||||
# A previous run (podman restart, systemd Restart=) leaves a stale PID file
|
||||
# and socket behind; avahi-daemon then refuses to start, which under `set -e`
|
||||
# would take the whole container down.
|
||||
rm -f /run/avahi-daemon/pid /run/avahi-daemon/socket
|
||||
if ! avahi-daemon --daemonize --no-chroot; then
|
||||
log "warning: avahi-daemon failed to start; AirPrint discovery will not work"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- TLS certificate for IPPS (AirPrint) ------------------------------------
|
||||
# CUPS advertises the secure _ipps._tcp service and both macOS and iOS prefer
|
||||
# it. Clients validate the certificate against the name they connected to, but
|
||||
# the certificate CUPS generates on demand is named after the container's host
|
||||
# name - which is not necessarily the name Avahi advertises (<host>.local).
|
||||
# A mismatch makes the secure add fail, and macOS then silently falls back to
|
||||
# its "Generic PostScript Printer" driver (no duplex, no colour, no trays).
|
||||
# Install our own certificate with SANs covering every name this server answers
|
||||
# to, and point cupsd at it (see ensure_cupsd_cert_directives below).
|
||||
CERT_DIR="${CERT_DIR:-/etc/cups/ssl}"
|
||||
CERT_CRT="${CERT_DIR}/server.crt"
|
||||
CERT_KEY="${CERT_DIR}/server.key"
|
||||
CERT_SAN_FILE="${CERT_DIR}/server.san"
|
||||
CERT_DAYS="${CERT_DAYS:-3650}"
|
||||
CERT_EXTRA_NAMES="${CERT_EXTRA_NAMES:-}"
|
||||
|
||||
# FQDN Avahi advertises for this host, e.g. "podp.local" - the name clients
|
||||
# really connect to. Requires the D-Bus system bus and avahi-daemon above.
|
||||
avahi_fqdn() {
|
||||
dbus-send --system --print-reply --dest=org.freedesktop.Avahi \
|
||||
/ org.freedesktop.Avahi.Server.GetHostNameFqdn 2>/dev/null \
|
||||
| sed -n 's/.*string "\(.*\)"/\1/p' | head -n 1 | sed 's/\.$//'
|
||||
}
|
||||
|
||||
# Every name a client may use for this server, as certificate SAN entries.
|
||||
# An entry is treated as an IP address when it only contains digits, dots and
|
||||
# colons; anything else is treated as a DNS name.
|
||||
cert_san_list() {
|
||||
printf '%s\n' "DNS:localhost" "DNS:localhost.localdomain" "IP:127.0.0.1"
|
||||
printf '%s\n' "DNS:host.containers.internal"
|
||||
if [ -n "${CERT_FQDN}" ]; then
|
||||
printf 'DNS:%s\n' "${CERT_FQDN}"
|
||||
fi
|
||||
if [ -n "${CERT_HOSTNAME}" ]; then
|
||||
printf 'DNS:%s\n' "${CERT_HOSTNAME}"
|
||||
case "${CERT_HOSTNAME}" in
|
||||
*.*) ;;
|
||||
*) printf 'DNS:%s.local\n' "${CERT_HOSTNAME}" ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s\n' "${CERT_EXTRA_NAMES}" | tr ',' '\n' | while IFS= read -r name; do
|
||||
[ -n "${name}" ] || continue
|
||||
case "${name}" in
|
||||
*[!0-9.:]*) printf 'DNS:%s\n' "${name}" ;;
|
||||
*) printf 'IP:%s\n' "${name}" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cert_common_name() {
|
||||
if [ -n "${CERT_HOSTNAME}" ]; then
|
||||
printf '%s' "${CERT_HOSTNAME}"
|
||||
else
|
||||
printf 'localhost'
|
||||
fi
|
||||
}
|
||||
|
||||
# Install a copy of the certificate where cupsd looks by default, i.e.
|
||||
# <name>.crt/.key in the certificate directory, for every name this server
|
||||
# could be known as (host name and Avahi FQDN). CUPS generates its own
|
||||
# certificate there and, at least in CUPS 2.4, keeps serving that file even when
|
||||
# ServerCertificate points elsewhere - the copies make sure the SANs are covered
|
||||
# either way. Re-installed on every start because cupsd may have replaced them.
|
||||
install_default_cert_copy() {
|
||||
[ -s "${CERT_CRT}" ] || return 0
|
||||
for name in "${CERT_HOSTNAME}" "${CERT_FQDN}"; do
|
||||
case "${name}" in
|
||||
"" | */*) continue ;;
|
||||
esac
|
||||
cp -f "${CERT_CRT}" "${CERT_DIR}/${name}.crt" 2>/dev/null || continue
|
||||
cp -f "${CERT_KEY}" "${CERT_DIR}/${name}.key" 2>/dev/null || continue
|
||||
chmod 0644 "${CERT_DIR}/${name}.crt" 2>/dev/null || true
|
||||
chmod 0640 "${CERT_DIR}/${name}.key" 2>/dev/null || true
|
||||
chown root:lp "${CERT_DIR}/${name}.crt" "${CERT_DIR}/${name}.key" 2>/dev/null || true
|
||||
done
|
||||
}
|
||||
|
||||
# Generate the certificate unless the existing one already covers every wanted
|
||||
# SAN entry. Only *missing* names trigger a regeneration (extra SANs are
|
||||
# harmless, and reissuing on every start would force clients that trusted the
|
||||
# certificate to trust it again), sorted so the order of discovery does not
|
||||
# matter. Changing CERT_EXTRA_NAMES (or the host name) takes effect next start.
|
||||
ensure_certificate() {
|
||||
sans="$(cert_san_list | sort -u | tr '\n' ',' | sed 's/,$//')"
|
||||
|
||||
if [ -s "${CERT_CRT}" ] && [ -s "${CERT_KEY}" ] && [ -f "${CERT_SAN_FILE}" ]; then
|
||||
have=",$(cat "${CERT_SAN_FILE}"),"
|
||||
complete=1
|
||||
for entry in $(printf '%s' "${sans}" | tr ',' '\n'); do
|
||||
case "${have}" in
|
||||
*",${entry},"*) ;;
|
||||
*)
|
||||
complete=0
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [ "${complete}" -eq 1 ]; then
|
||||
log "TLS certificate is up to date (${CERT_CRT})"
|
||||
install_default_cert_copy
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "${CERT_DIR}" 2>/dev/null || true
|
||||
if [ ! -w "${CERT_DIR}" ]; then
|
||||
log "warning: ${CERT_DIR} is not writable; falling back to the certificate CUPS generates itself"
|
||||
log "warning: iOS/macOS may then reject the secure _ipps connection (see README)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "generating self-signed TLS certificate for IPPS (${CERT_DAYS} days)"
|
||||
# basicConstraints=CA:TRUE so the same file can be imported as a trusted
|
||||
# root on macOS/iOS; the key/cert are otherwise only used for this server.
|
||||
if openssl req -x509 -newkey rsa:2048 -sha256 -nodes -days "${CERT_DAYS}" \
|
||||
-keyout "${CERT_KEY}" -out "${CERT_CRT}" \
|
||||
-subj "/CN=$(cert_common_name)" \
|
||||
-addext "subjectAltName=${sans}" \
|
||||
-addext "basicConstraints=critical,CA:TRUE" \
|
||||
-addext "subjectKeyIdentifier=hash" >/dev/null 2>&1; then
|
||||
chmod 0644 "${CERT_CRT}"
|
||||
chmod 0640 "${CERT_KEY}"
|
||||
chown root:lp "${CERT_CRT}" "${CERT_KEY}" 2>/dev/null || true
|
||||
printf '%s' "${sans}" > "${CERT_SAN_FILE}"
|
||||
log "certificate SANs: ${sans}"
|
||||
install_default_cert_copy
|
||||
else
|
||||
log "warning: could not generate a certificate; CUPS will generate its own"
|
||||
rm -f "${CERT_CRT}" "${CERT_KEY}"
|
||||
fi
|
||||
}
|
||||
|
||||
# Point cupsd at our certificate. Only done when the files exist: cupsd refuses
|
||||
# to start when ServerCertificate cannot be read. Skipped when the directives
|
||||
# are already present, so this also repairs a cupsd.conf previously rewritten by
|
||||
# cupsctl (which drops directives it does not manage).
|
||||
ensure_cupsd_cert_directives() {
|
||||
conf=/etc/cups/cupsd.conf
|
||||
[ -f "${conf}" ] || return 0
|
||||
if [ -s "${CERT_CRT}" ] && [ -s "${CERT_KEY}" ] \
|
||||
&& ! grep -qE '^[[:space:]]*ServerCertificate[[:space:]]' "${conf}"; then
|
||||
if [ -w "${conf}" ]; then
|
||||
log "adding ServerCertificate/ServerKey to ${conf}"
|
||||
printf '\n# TLS certificate for IPPS (AirPrint). See entrypoint.sh.\nServerCertificate %s\nServerKey %s\n' \
|
||||
"${CERT_CRT}" "${CERT_KEY}" >> "${conf}"
|
||||
else
|
||||
log "warning: ${conf} is not writable; cannot point cupsd at the certificate"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
CERT_HOSTNAME="$(hostname 2>/dev/null || true)"
|
||||
CERT_FQDN="$(avahi_fqdn || true)"
|
||||
ensure_certificate
|
||||
ensure_cupsd_cert_directives
|
||||
|
||||
# --- CUPS -------------------------------------------------------------------
|
||||
log "starting cupsd"
|
||||
/usr/sbin/cupsd -f &
|
||||
|
||||
Reference in new issue
Block a user