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:
@@ -3,3 +3,6 @@
|
|||||||
# Extra CA certificates dropped in for building behind a proxy
|
# Extra CA certificates dropped in for building behind a proxy
|
||||||
koha/certs/*.crt
|
koha/certs/*.crt
|
||||||
quickadd/certs/*.crt
|
quickadd/certs/*.crt
|
||||||
|
# Generated by install.sh --domain
|
||||||
|
Caddyfile
|
||||||
|
caddy-root.crt
|
||||||
|
|||||||
@@ -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
|
**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
|
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.
|
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`.
|
||||||
|
|||||||
@@ -77,7 +77,26 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- quickadd-data:/data
|
- 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:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
koha-data:
|
koha-data:
|
||||||
quickadd-data:
|
quickadd-data:
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
# sudo ./install.sh --status # show URLs and container health
|
# sudo ./install.sh --status # show URLs and container health
|
||||||
# sudo ./install.sh --update # rebuild with the latest Koha and restart
|
# sudo ./install.sh --update # rebuild with the latest Koha and restart
|
||||||
# sudo ./install.sh --stop | --start
|
# 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:
|
# Optional environment variables:
|
||||||
# LIBRARY_NAME="Smith Family Library" name shown on the catalogue
|
# LIBRARY_NAME="Smith Family Library" name shown on the catalogue
|
||||||
@@ -79,6 +82,10 @@ show_status() {
|
|||||||
fi
|
fi
|
||||||
dc ps
|
dc ps
|
||||||
echo
|
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 " 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 " 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)"
|
echo " Admin login: $ADMIN_USER / $ADMIN_PASSWORD (also stored in $HERE/.env)"
|
||||||
@@ -90,6 +97,52 @@ show_status() {
|
|||||||
echo
|
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
|
case "${1:-}" in
|
||||||
--status) show_status; exit 0 ;;
|
--status) show_status; exit 0 ;;
|
||||||
--stop) dc stop; exit 0 ;;
|
--stop) dc stop; exit 0 ;;
|
||||||
@@ -152,6 +205,8 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
[ -n "$DOMAIN_ARG" ] && configure_caddy
|
||||||
|
|
||||||
if [ -n "${EXTRA_CA:-}" ]; then
|
if [ -n "${EXTRA_CA:-}" ]; then
|
||||||
cp "$EXTRA_CA" koha/certs/extra-ca.crt
|
cp "$EXTRA_CA" koha/certs/extra-ca.crt
|
||||||
cp "$EXTRA_CA" quickadd/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; }
|
[ "${READY:-0}" = 1 ] || { c_err "Timed out - see: docker logs -f library-koha"; exit 1; }
|
||||||
|
|
||||||
c_ok "Your library is running."
|
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
|
echo
|
||||||
show_status
|
show_status
|
||||||
|
|||||||
@@ -1720,6 +1720,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.send_header("Cache-Control", "no-store")
|
self.send_header("Cache-Control", "no-store")
|
||||||
self.send_header("X-Content-Type-Options", "nosniff")
|
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("Referrer-Policy", "no-referrer")
|
||||||
self.send_header("Content-Security-Policy",
|
self.send_header("Content-Security-Policy",
|
||||||
"default-src 'self'; img-src 'self' https: data:; "
|
"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.
|
# Otherwise: a patron signing in with their card number (or userid) and password.
|
||||||
addr = self.client_address[0]
|
addr = self.client_address[0]
|
||||||
if SSO_SECRET and hmac.compare_digest(self.headers.get("X-Quickadd-Proxy", "").encode(), SSO_SECRET.encode()):
|
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
|
self.addr = addr
|
||||||
pid = patron_login(u, p, addr)
|
pid = patron_login(u, p, addr)
|
||||||
if pid:
|
if pid:
|
||||||
|
|||||||
Reference in New Issue
Block a user