library: HTTPS via Caddy (install.sh --domain [--lan-tls]) so the live camera works; real visitor address behind a proxy; camera permission header

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
This commit is contained in:
Claude
2026-11-07 03:45:26 +00:00
parent 9fb512cdab
commit 3ef968fd10
5 changed files with 107 additions and 1 deletions
+3
View File
@@ -3,3 +3,6 @@
# Extra CA certificates dropped in for building behind a proxy
koha/certs/*.crt
quickadd/certs/*.crt
# Generated by install.sh --domain
Caddyfile
caddy-root.crt
+17
View File
@@ -297,3 +297,20 @@ New installs of this stack use the same three ports whether you install it from
**8098** is the one to use (the Room for Books home page, with Koha behind the menu), 8099 is Add books direct, and 8097 is the
public catalogue, which stays closed. If one is taken, the next free port is used and the installer prints it. Existing installs
keep the ports they already have.
## HTTPS with Caddy (and the live camera scanner)
Phones only allow a live camera on an HTTPS page. One command adds Caddy in front of the library:
```bash
# a real domain that points at this machine (ports 80 and 443 reachable): the certificate is automatic
sudo ./install.sh --domain library.example.com
# a name only your own network knows (library.home.lan): Caddy makes its own certificate authority
sudo ./install.sh --domain library.home.lan --lan-tls # then install caddy-root.crt on your phones
```
It writes `Caddyfile`, starts a `caddy` container (the compose `caddy` profile), sets `STAFF_URL=https://<name>` and closes the plain-HTTP
ports to the network (`BIND_ADDR=127.0.0.1`; add `--keep-direct` to leave them open). Then use `https://<name>/`: everything works
as before, and the **Live scan** buttons appear on Add books and Lend / return. Behind your own Caddy instead, point it at the library
port: `library.example.com { reverse_proxy <this machine>:8098 }` and set `STAFF_URL=https://library.example.com` in `.env`.
+19
View File
@@ -77,7 +77,26 @@ services:
volumes:
- quickadd-data:/data
# HTTPS with an automatic certificate (and so the live camera on phones): only started when
# `./install.sh --domain <name>` has set COMPOSE_PROFILES=caddy in .env.
caddy:
profiles: ["caddy"]
image: caddy:2
container_name: library-caddy
restart: unless-stopped
depends_on:
- koha
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
volumes:
db-data:
koha-data:
quickadd-data:
caddy-data:
caddy-config:
+62
View File
@@ -5,6 +5,9 @@
# sudo ./install.sh --status # show URLs and container health
# sudo ./install.sh --update # rebuild with the latest Koha and restart
# sudo ./install.sh --stop | --start
# sudo ./install.sh --domain library.example.com # HTTPS via Caddy (certificate is automatic)
# sudo ./install.sh --domain library.home.lan --lan-tls # HTTPS for a name only your network knows (own CA)
# add --keep-direct to also keep the plain-HTTP ports open on the network
#
# Optional environment variables:
# LIBRARY_NAME="Smith Family Library" name shown on the catalogue
@@ -79,6 +82,10 @@ show_status() {
fi
dc ps
echo
if [ -n "${DOMAIN:-}" ]; then
echo " Your library (HTTPS): https://$DOMAIN <- use this one: the live camera scanner needs it"
echo " (Koha's own screens: https://$DOMAIN/cgi-bin/koha/mainpage.pl, or the menu inside)"
fi
echo " Your library: http://$ip:$STAFF_PORT"
echo " sign in here: find a book, add books (scan an ISBN or use the phone camera), lending, labels, people"
echo " Admin login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env)"
@@ -90,6 +97,52 @@ show_status() {
echo
}
# --- HTTPS (Caddy) options --------------------------------------------------------
DOMAIN_ARG="" LAN_TLS=0 KEEP_DIRECT=0 ACTION=""
while [ $# -gt 0 ]; do
case "$1" in
--domain) DOMAIN_ARG="${2:-}"; [ -n "$DOMAIN_ARG" ] || { c_err "--domain needs a name"; exit 1; }; shift 2 ;;
--lan-tls) LAN_TLS=1; shift ;;
--keep-direct) KEEP_DIRECT=1; shift ;;
*) ACTION="${ACTION:+$ACTION }$1"; shift ;;
esac
done
set -- $ACTION
if [ -n "$DOMAIN_ARG" ] && ! printf '%s' "$DOMAIN_ARG" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9.-]*[A-Za-z0-9])?$'; then
c_err "That does not look like a host name: $DOMAIN_ARG"; exit 1
fi
# Write (or replace) KEY=VALUE in .env
env_set() { if grep -q "^$1=" .env; then sed -i "s|^$1=.*|$1=$2|" .env; else echo "$1=$2" >> .env; fi; }
configure_caddy() {
local name="$DOMAIN_ARG"
# Caddy needs 80 and 443 (it is not moved to other ports: browsers expect these).
if ! dc ps --status running --services 2>/dev/null | grep -qx caddy; then
for p in 80 443; do
port_in_use "$p" && { c_err "Port $p is already in use on this machine - Caddy needs it. Stop what is using it, or use your own reverse proxy (see README)."; exit 1; }
done
fi
{
echo "# Generated by install.sh --domain - Caddy fetches and renews the certificate by itself."
echo "$name {"
[ "$LAN_TLS" = 1 ] && echo " tls internal"
echo " encode zstd gzip"
echo " reverse_proxy koha:8081"
echo "}"
} > Caddyfile
env_set DOMAIN "$name"
env_set COMPOSE_PROFILES caddy
env_set STAFF_URL "https://$name"
[ "$KEEP_DIRECT" = 1 ] || env_set BIND_ADDR 127.0.0.1
c_ok "Caddy will serve https://$name"
if [ "$LAN_TLS" = 1 ]; then
c_info "Own-CA certificate: phones must trust Caddy's root, which is copied to $HERE/caddy-root.crt after start."
else
c_info "The name must point at this machine (DNS) and ports 80/443 must reach it, or the certificate cannot be issued."
fi
}
case "${1:-}" in
--status) show_status; exit 0 ;;
--stop) dc stop; exit 0 ;;
@@ -152,6 +205,8 @@ else
fi
fi
[ -n "$DOMAIN_ARG" ] && configure_caddy
if [ -n "${EXTRA_CA:-}" ]; then
cp "$EXTRA_CA" koha/certs/extra-ca.crt
cp "$EXTRA_CA" quickadd/certs/extra-ca.crt
@@ -180,5 +235,12 @@ done
[ "${READY:-0}" = 1 ] || { c_err "Timed out - see: docker logs -f library-koha"; exit 1; }
c_ok "Your library is running."
if [ -n "${DOMAIN:-}" ] && grep -q '^ tls internal' Caddyfile 2>/dev/null; then
for i in $(seq 1 20); do
docker cp library-caddy:/data/caddy/pki/authorities/local/root.crt "$HERE/caddy-root.crt" >/dev/null 2>&1 && break
sleep 2
done
[ -f "$HERE/caddy-root.crt" ] && c_info "Install this certificate on phones/computers that should trust https://$DOMAIN: $HERE/caddy-root.crt"
fi
echo
show_status
+6 -1
View File
@@ -1720,6 +1720,7 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Permissions-Policy", "camera=(self), microphone=()")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Content-Security-Policy",
"default-src 'self'; img-src 'self' https: data:; "
@@ -1754,7 +1755,11 @@ class Handler(BaseHTTPRequestHandler):
# Otherwise: a patron signing in with their card number (or userid) and password.
addr = self.client_address[0]
if SSO_SECRET and hmac.compare_digest(self.headers.get("X-Quickadd-Proxy", "").encode(), SSO_SECRET.encode()):
addr = self.headers.get("X-Forwarded-For", "").split(",")[-1].strip() or addr
# Behind Caddy the list reads "<visitor>, <Caddy>" (Caddy adds the visitor, the staff site's web server
# adds Caddy), so the visitor is the one before the last. Used only to count failed sign-ins per
# visitor, never to vouch for anyone.
hops = [h.strip() for h in self.headers.get("X-Forwarded-For", "").split(",") if h.strip()]
addr = (hops[-2] if len(hops) >= 2 else hops[-1]) if hops else addr
self.addr = addr
pid = patron_login(u, p, addr)
if pid: