#!/usr/bin/env bash # # bin/doorctl.sh — door unlock endpoint, systemd socket-activated. # # One instance per connection: systemd hands the accepted socket to # stdin/stdout and exports REMOTE_ADDR. Reads a single HTTP request # line; fires the door relay only for GET /unlock/ from an # allowlisted source. Responds BEFORE the hold so callers are never # blocked for the hold duration. # # Env (from /etc/default/doorman via the service unit): # DOORMAN_UNLOCK_TOKEN shared secret in the URL path (required) # DOORMAN_ALLOWED_SRC space-separated source IPs allowed to unlock # DOORMAN_RELAY_DEV usbrelay device.channel (e.g. 3X9XI_1) # DOORMAN_USBRELAY path to usbrelay binary # DOORMAN_HOLD relay hold seconds # set -u log() { logger -t doorctl -- "$1" 2>/dev/null || true; } respond() { printf '%s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n' "$1"; } deny() { respond 'HTTP/1.0 403 Forbidden' log "DENY from ${REMOTE_ADDR:-unknown}: $2" exit 0 } read -r reqline || true reqline=${reqline:-$'\r'} method=${reqline%% *} if [ "$method" != "GET" ]; then deny "bad method" fi rest=${reqline#* } path=${rest%% *} [ "$path" = "/unlock/${DOORMAN_UNLOCK_TOKEN:-}" ] || deny "bad path or token" allowed="${DOORMAN_ALLOWED_SRC:-}" if [ -n "$allowed" ]; then case " $allowed " in *" ${REMOTE_ADDR:-} "*) : ;; *) deny "source not allowlisted" ;; esac fi hold="${DOORMAN_HOLD:-10}" secure() { "${DOORMAN_USBRELAY:-/usr/bin/usbrelay}" "${DOORMAN_RELAY_DEV}=0" >/dev/null 2>&1; } # Self-securing: whatever kills this script (systemd timeout, OOM, signal), # the relay returns to 0 = door secure. Never rely on the happy path alone. trap secure EXIT log "UNLOCK from ${REMOTE_ADDR:-unknown}: ${DOORMAN_RELAY_DEV}=1 for ${hold}s" respond 'HTTP/1.0 200 OK' "${DOORMAN_USBRELAY:-/usr/bin/usbrelay}" "${DOORMAN_RELAY_DEV}=1" >/dev/null 2>&1 sleep "$hold" secure log "secure: ${DOORMAN_RELAY_DEV}=0"