feat(doorman): doorctl unlock endpoint — HA->Pi callback over Tailscale [#345]
Founder-approved 2026-09-03 (relay load side disconnected during build). systemd socket-activated, Tailscale-bound, token+source allowlist, EXIT-trap self-securing relay. 9/9 doorctl tests (auth matrix + relay cycle, mocked relay), full suite + shellcheck clean. https://projects.knownelement.com/issues/345
This commit is contained in:
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/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/<token> 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"
|
||||
Reference in New Issue
Block a user