mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
fix(install): preserve the existing network binding on updates and re-installs
Updating must never silently loosen security. The update path already never rewrites service files; this covers the remaining gap, re-running the full installer over an existing setup: - read_existing_binding() parses the current systemd unit or launchd plist (a pre-1.8 service without our env lines counts as loopback). - The network-access prompt defaults to the CURRENT setup instead of the network default, shows what that setup is, and Enter keeps it, including a custom non-loopback host and the existing password. - Non-interactive re-installs adopt the existing binding wholesale. - The update path's closing security notice now reflects the service's actual binding instead of the generic loopback text. Round-trip escaping tested for both formats (quotes, backslashes, XML specials) plus the legacy-unit, preserve, and Enter-keeps flows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+97
-9
@@ -43,6 +43,14 @@ BIND_HOST=""
|
|||||||
BIND_PASSWORD=""
|
BIND_PASSWORD=""
|
||||||
BIND_ACK="0"
|
BIND_ACK="0"
|
||||||
|
|
||||||
|
# Binding found in an already-installed service (read_existing_binding), used
|
||||||
|
# so updates and re-installs preserve the user's previous choice instead of
|
||||||
|
# silently loosening it to the new network-access default.
|
||||||
|
EXISTING_FOUND="0"
|
||||||
|
EXISTING_HOST=""
|
||||||
|
EXISTING_PASSWORD=""
|
||||||
|
EXISTING_ACK="0"
|
||||||
|
|
||||||
# puppeteer is a devDependency used only by scripts/browser-comparison.mjs — its
|
# puppeteer is a devDependency used only by scripts/browser-comparison.mjs — its
|
||||||
# ~150MB chrome-headless-shell download is never needed to build or run Codeman.
|
# ~150MB chrome-headless-shell download is never needed to build or run Codeman.
|
||||||
# Skipping it avoids a slow download and a fatal install failure when a prior
|
# Skipping it avoids a slow download and a fatal install failure when a prior
|
||||||
@@ -997,6 +1005,44 @@ xml_escape() {
|
|||||||
printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'
|
printf '%s' "$1" | sed -e 's/&/\&/g' -e 's/</\</g' -e 's/>/\>/g'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemd_env_unescape() {
|
||||||
|
printf '%s' "$1" | sed 's/\\\(["\\]\)/\1/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
xml_unescape() {
|
||||||
|
printf '%s' "$1" | sed -e 's/</</g' -e 's/>/>/g' -e 's/&/\&/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read the binding out of an already-installed service file, if any. A service
|
||||||
|
# file WITHOUT our CODEMAN_HOST line is a pre-1.8 install, which effectively
|
||||||
|
# ran loopback (the server default), so it reports 127.0.0.1.
|
||||||
|
read_existing_binding() {
|
||||||
|
EXISTING_FOUND="0"; EXISTING_HOST=""; EXISTING_PASSWORD=""; EXISTING_ACK="0"
|
||||||
|
local unit="$HOME/.config/systemd/user/codeman-web.service"
|
||||||
|
local plist="$HOME/Library/LaunchAgents/com.codeman.web.plist"
|
||||||
|
|
||||||
|
if [[ -f "$unit" ]]; then
|
||||||
|
EXISTING_FOUND="1"
|
||||||
|
EXISTING_HOST=$(sed -n 's/^Environment=CODEMAN_HOST=//p' "$unit" | head -1)
|
||||||
|
local pwline
|
||||||
|
pwline=$(sed -n 's/^Environment="CODEMAN_PASSWORD=\(.*\)"$/\1/p' "$unit" | head -1)
|
||||||
|
[[ -n "$pwline" ]] && EXISTING_PASSWORD=$(systemd_env_unescape "$pwline")
|
||||||
|
grep -q '^Environment=CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1' "$unit" && EXISTING_ACK="1"
|
||||||
|
elif [[ -f "$plist" ]]; then
|
||||||
|
EXISTING_FOUND="1"
|
||||||
|
EXISTING_HOST=$(awk '/<key>CODEMAN_HOST<\/key>/{getline; print}' "$plist" | sed -n 's/.*<string>\(.*\)<\/string>.*/\1/p')
|
||||||
|
local pwraw
|
||||||
|
pwraw=$(awk '/<key>CODEMAN_PASSWORD<\/key>/{getline; print}' "$plist" | sed -n 's/.*<string>\(.*\)<\/string>.*/\1/p')
|
||||||
|
[[ -n "$pwraw" ]] && EXISTING_PASSWORD=$(xml_unescape "$pwraw")
|
||||||
|
grep -q '<key>CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK</key>' "$plist" && EXISTING_ACK="1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$EXISTING_FOUND" == "1" && -z "$EXISTING_HOST" ]]; then
|
||||||
|
EXISTING_HOST="127.0.0.1"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# Ask how the dashboard should be reachable and set BIND_HOST/BIND_PASSWORD/
|
# Ask how the dashboard should be reachable and set BIND_HOST/BIND_PASSWORD/
|
||||||
# BIND_ACK. Interactive default is network access (0.0.0.0) because that is
|
# BIND_ACK. Interactive default is network access (0.0.0.0) because that is
|
||||||
# what most installs need; loopback is offered as the safer alternative.
|
# what most installs need; loopback is offered as the safer alternative.
|
||||||
@@ -1014,28 +1060,49 @@ choose_network_binding() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# A previous install's choice is the baseline: re-installing must never
|
||||||
|
# silently loosen it.
|
||||||
|
read_existing_binding
|
||||||
|
|
||||||
if [[ "$NONINTERACTIVE" == "1" ]] || ! has_tty; then
|
if [[ "$NONINTERACTIVE" == "1" ]] || ! has_tty; then
|
||||||
BIND_HOST="127.0.0.1"
|
if [[ "$EXISTING_FOUND" == "1" ]]; then
|
||||||
info "Non-interactive install: binding 127.0.0.1 (preset CODEMAN_HOST=0.0.0.0 to override)"
|
BIND_HOST="$EXISTING_HOST"
|
||||||
|
BIND_PASSWORD="$EXISTING_PASSWORD"
|
||||||
|
BIND_ACK="$EXISTING_ACK"
|
||||||
|
info "Non-interactive install: preserving existing binding ($BIND_HOST)"
|
||||||
|
else
|
||||||
|
BIND_HOST="127.0.0.1"
|
||||||
|
info "Non-interactive install: binding 127.0.0.1 (preset CODEMAN_HOST=0.0.0.0 to override)"
|
||||||
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Default follows the existing setup when there is one, else network.
|
||||||
|
local default_choice="1"
|
||||||
|
if [[ "$EXISTING_FOUND" == "1" && "$EXISTING_HOST" == "127.0.0.1" ]]; then
|
||||||
|
default_choice="2"
|
||||||
|
fi
|
||||||
|
|
||||||
echo -e " ${BOLD}Network access${NC}"
|
echo -e " ${BOLD}Network access${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " How should the Codeman dashboard be reachable?"
|
echo -e " How should the Codeman dashboard be reachable?"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e " ${CYAN}1)${NC} ${BOLD}Any device on your network${NC} ${DIM}(0.0.0.0)${NC} [default]"
|
echo -e " ${CYAN}1)${NC} ${BOLD}Any device on your network${NC} ${DIM}(0.0.0.0)${NC}"
|
||||||
echo -e " Open it straight from your phone or laptop."
|
echo -e " Open it straight from your phone or laptop."
|
||||||
echo -e " ${YELLOW}Less safe: set a password so only you control your agents.${NC}"
|
echo -e " ${YELLOW}Less safe: set a password so only you control your agents.${NC}"
|
||||||
echo -e " ${CYAN}2)${NC} ${BOLD}This machine only${NC} ${DIM}(127.0.0.1)${NC}"
|
echo -e " ${CYAN}2)${NC} ${BOLD}This machine only${NC} ${DIM}(127.0.0.1)${NC}"
|
||||||
echo -e " Safest. Reach it remotely via Tailscale or a tunnel."
|
echo -e " Safest. Reach it remotely via Tailscale or a tunnel."
|
||||||
echo ""
|
echo ""
|
||||||
|
if [[ "$EXISTING_FOUND" == "1" ]]; then
|
||||||
|
echo -e " ${DIM}Current setup: $EXISTING_HOST$([[ -n "$EXISTING_PASSWORD" ]] && echo ", password set"). Enter keeps it.${NC}"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
local bind_choice=""
|
local bind_choice=""
|
||||||
while true; do
|
while true; do
|
||||||
echo -en "${CYAN}Choose [1/2] (default 1):${NC} " >&2
|
echo -en "${CYAN}Choose [1/2] (default $default_choice):${NC} " >&2
|
||||||
read_reply bind_choice || bind_choice="1"
|
read_reply bind_choice || bind_choice="$default_choice"
|
||||||
bind_choice="${bind_choice:-1}"
|
bind_choice="${bind_choice:-$default_choice}"
|
||||||
case "$bind_choice" in
|
case "$bind_choice" in
|
||||||
1|2) break ;;
|
1|2) break ;;
|
||||||
*) echo "Please enter 1 or 2." >&2 ;;
|
*) echo "Please enter 1 or 2." >&2 ;;
|
||||||
@@ -1048,7 +1115,13 @@ choose_network_binding() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BIND_HOST="0.0.0.0"
|
# Keep a custom non-loopback host from a previous install (e.g. a specific
|
||||||
|
# interface IP); otherwise bind all interfaces.
|
||||||
|
if [[ "$EXISTING_FOUND" == "1" && -n "$EXISTING_HOST" && "$EXISTING_HOST" != "127.0.0.1" ]]; then
|
||||||
|
BIND_HOST="$EXISTING_HOST"
|
||||||
|
else
|
||||||
|
BIND_HOST="0.0.0.0"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "${CODEMAN_PASSWORD:-}" ]]; then
|
if [[ -n "${CODEMAN_PASSWORD:-}" ]]; then
|
||||||
BIND_PASSWORD="$CODEMAN_PASSWORD"
|
BIND_PASSWORD="$CODEMAN_PASSWORD"
|
||||||
@@ -1057,11 +1130,17 @@ choose_network_binding() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
local pw="" pw2=""
|
local pw="" pw2="" keep_hint=""
|
||||||
|
[[ -n "$EXISTING_PASSWORD" ]] && keep_hint="Enter to keep the current one" || keep_hint="Enter to skip"
|
||||||
while true; do
|
while true; do
|
||||||
echo -en "${CYAN}Set a dashboard password (recommended; Enter to skip):${NC} " >&2
|
echo -en "${CYAN}Set a dashboard password (recommended; $keep_hint):${NC} " >&2
|
||||||
read_secret pw || pw=""
|
read_secret pw || pw=""
|
||||||
if [[ -z "$pw" ]]; then
|
if [[ -z "$pw" ]]; then
|
||||||
|
if [[ -n "$EXISTING_PASSWORD" ]]; then
|
||||||
|
BIND_PASSWORD="$EXISTING_PASSWORD"
|
||||||
|
success "Keeping the existing password"
|
||||||
|
break
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
warn "Without a password, EVERY device on your network gets full access"
|
warn "Without a password, EVERY device on your network gets full access"
|
||||||
warn "to your agents (they run commands as $USER)."
|
warn "to your agents (they run commands as $USER)."
|
||||||
@@ -1854,6 +1933,15 @@ update() {
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Reflect the service's actual binding in the closing notice. Updates
|
||||||
|
# never rewrite the service files, so the existing choice is authoritative.
|
||||||
|
read_existing_binding
|
||||||
|
if [[ "$EXISTING_FOUND" == "1" ]]; then
|
||||||
|
BIND_HOST="$EXISTING_HOST"
|
||||||
|
BIND_PASSWORD="$EXISTING_PASSWORD"
|
||||||
|
BIND_ACK="$EXISTING_ACK"
|
||||||
|
fi
|
||||||
|
|
||||||
print_security_notice
|
print_security_notice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user