diff --git a/SSH_and_GPG/create_ssh_key.sh b/SSH_and_GPG/create_ssh_key.sh index 69acaac..873722a 100755 --- a/SSH_and_GPG/create_ssh_key.sh +++ b/SSH_and_GPG/create_ssh_key.sh @@ -81,6 +81,11 @@ fi prompt KEY_NAME "Key file name (no path)" "${GIT_HOST%%.*}" +if [[ "$KEY_NAME" == "." || "$KEY_NAME" == ".." || ! "$KEY_NAME" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then + echo "Error: KEY_NAME must be a simple key file name without a path, whitespace, or control characters." >&2 + exit 1 +fi + # ---- Optional passphrase ---- SSH_PASSPHRASE="${SSH_PASSPHRASE:-}" if [[ -z "$SSH_PASSPHRASE" ]]; then @@ -115,6 +120,33 @@ if [[ -n "$SSH_PASSPHRASE" ]]; then fi fi +validate_host_token() { + local value="$1" label="$2" + if [[ -z "$value" ]]; then + printf 'Error: %s cannot be empty.\n' "$label" >&2 + exit 1 + fi + if [[ "$value" == *://* || "$value" == *[[:space:][:cntrl:]]* || "$value" == -* || "$value" == *\** || "$value" == *\?* || "$value" == *\!* || "$value" == */* ]]; then + printf 'Error: %s must be one literal SSH host token, not a URL, wildcard, negated pattern, or value containing whitespace/control characters.\n' "$label" >&2 + exit 1 + fi +} + +if [[ -n "$GIT_HOSTNAME" && -z "$IS_SELF_HOSTED" ]]; then + echo "Error: GIT_HOSTNAME requires IS_SELF_HOSTED=true." >&2 + exit 1 +fi + +validate_host_token "$GIT_HOST" "GIT_HOST" +if [[ -n "$IS_SELF_HOSTED" ]]; then + validate_host_token "$GIT_HOSTNAME" "GIT_HOSTNAME" + GIT_SSH_PORT="${GIT_SSH_PORT:-22}" + if [[ ! "$GIT_SSH_PORT" =~ ^[0-9]+$ ]] || (( GIT_SSH_PORT < 1 || GIT_SSH_PORT > 65535 )); then + echo "Error: GIT_SSH_PORT must be a number from 1 to 65535." >&2 + exit 1 + fi +fi + SSH_DIR="$HOME/.ssh" KEY_PATH="$SSH_DIR/$KEY_NAME" PUB_PATH="$KEY_PATH.pub" @@ -175,38 +207,62 @@ fi touch "$CFG_PATH" chmod 600 "$CFG_PATH" -# Remove any existing block for this host (simple, robust approach). -# This deletes from line "Host " up to the next "Host " line (or EOF). -tmp_cfg="$(mktemp)" -awk -v host="$GIT_HOST" ' - BEGIN {skip=0} - $1=="Host" && $2==host {skip=1; next} - $1=="Host" && skip==1 {skip=0} - skip==0 {print} +managed_begin="# BEGIN create_ssh_key.sh: $KEY_NAME" +legacy_begin='# BEGIN create_ssh_key.sh' +legacy_end='# END create_ssh_key.sh' +managed_end="# END create_ssh_key.sh: $KEY_NAME" +tmp_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")" +trap 'rm -f "$tmp_cfg"' EXIT +awk -v begin="$managed_begin" -v legacy_begin="$legacy_begin" -v legacy_end="$legacy_end" -v end="$managed_end" ' + function flush_block( i) { + for (i = 1; i <= block_lines; i++) print block[i] + block_lines = 0 + } + ($0 == begin || $0 == legacy_begin) { + in_block = 1 + block_lines = 1 + block[block_lines] = $0 + next + } + in_block { + block[++block_lines] = $0 + if ($0 == end || $0 == legacy_end) { + in_block = 0 + block_lines = 0 + } + next + } + {print} + END { + if (in_block) flush_block() + } ' "$CFG_PATH" > "$tmp_cfg" -mv "$tmp_cfg" "$CFG_PATH" -if [[ -n "$IS_SELF_HOSTED" ]]; then - { - echo "" - echo "Host $GIT_HOST" - echo " HostName $GIT_HOSTNAME" - # Port 22 is the SSH default — only emit the line for a non-standard port. - [[ -n "$GIT_SSH_PORT" && "$GIT_SSH_PORT" != "22" ]] && echo " Port $GIT_SSH_PORT" - echo " User git" - echo " AddKeysToAgent $ADD_KEYS_TO_AGENT" - echo " IdentityFile $KEY_PATH" - } >> "$CFG_PATH" -else - { - echo "" - echo "Host $GIT_HOST" - echo " AddKeysToAgent $ADD_KEYS_TO_AGENT" - # macOS keychain optional: - # echo " UseKeychain yes" - echo " IdentityFile $KEY_PATH" - } >> "$CFG_PATH" -fi +managed_block="$(mktemp "$SSH_DIR/config-block.XXXXXX")" +final_cfg="$(mktemp "$SSH_DIR/config.XXXXXX")" +trap 'rm -f "$tmp_cfg" "$managed_block" "$final_cfg"' EXIT +{ + printf '%s\n' "$managed_begin" + if [[ -n "$IS_SELF_HOSTED" ]]; then + printf 'Host %s %s\n' "$GIT_HOST" "$GIT_HOSTNAME" + printf ' HostName %s\n' "$GIT_HOSTNAME" + [[ "$GIT_SSH_PORT" != "22" ]] && printf ' Port %s\n' "$GIT_SSH_PORT" + printf ' User git\n' + else + printf 'Host %s\n' "$GIT_HOST" + fi + printf ' AddKeysToAgent %s\n' "$ADD_KEYS_TO_AGENT" + printf ' IdentityFile %s\n' "$KEY_PATH" + printf '%s\n' "$managed_end" +} > "$managed_block" + +{ + cat "$managed_block" + cat "$tmp_cfg" +} > "$final_cfg" +mv "$final_cfg" "$CFG_PATH" +rm -f "$tmp_cfg" "$managed_block" +trap - EXIT # ---- Show public key ---- echo "" diff --git a/lib/core.sh b/lib/core.sh index 3a8d101..a12b941 100755 --- a/lib/core.sh +++ b/lib/core.sh @@ -237,6 +237,37 @@ run_eval() { fi } +core_prime_sudo() { + if [[ "$DRY_RUN" == true ]]; then + printf ' [dry-run] sudo -v (cache credentials + background keepalive)\n' + return 0 + fi + printf '==> Caching sudo credentials (you may be prompted once)...\n' + if ! sudo -v; then + printf 'error: sudo authentication is required for this setup.\n' >&2 + printf ' Run it from an interactive terminal, or authenticate first with: sudo -v\n' >&2 + return 1 + fi + ( while true; do sudo -n true || true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) >/dev/null 2>&1 & +} + +prepare_nvm_environment() { + local npmrc="$HOME/.npmrc" backup tmp + unset NPM_CONFIG_PREFIX npm_config_prefix NPM_CONFIG_GLOBALCONFIG npm_config_globalconfig + [[ -f "$npmrc" ]] || return 0 + if ! grep -Eq '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$npmrc"; then + return 0 + fi + backup="${npmrc}.nvm-preflight.bak" + if [[ ! -e "$backup" ]]; then + cp "$npmrc" "$backup" + fi + tmp="$(mktemp)" + grep -Ev '^[[:space:]]*(prefix|globalconfig)[[:space:]]*=' "$npmrc" > "$tmp" || true + mv "$tmp" "$npmrc" + printf ' removed npm prefix settings incompatible with nvm (backup: %s)\n' "$backup" +} + # npm supply-chain cooldown: refuse to install package versions younger than # NPM_MIN_RELEASE_AGE days. Compromised releases of popular packages (e.g. the # axios RAT, Mar 2026) are typically caught and yanked within hours, so a short @@ -743,6 +774,7 @@ linux_nvm_flow() { configure_pnpm return 0 fi + prepare_nvm_environment if [ ! -d "$HOME/.nvm" ]; then printf '==> Installing nvm...\n' eval "$(custom_cmd nvm)" @@ -829,8 +861,10 @@ desktop_pipx_section() { desktop_pnpm_section() { printf '\n==> Installing pnpm packages...\n' - export NVM_DIR="$HOME/.nvm" - [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + if [[ "$DRY_RUN" == false ]]; then + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" + fi export PNPM_HOME="$HOME/.local/share/pnpm" export PATH="$PNPM_HOME/bin:$PATH" if command -v pnpm &>/dev/null; then @@ -879,6 +913,7 @@ linux_main() { else CONFIG_SRC_DIR="$SETUP_ROOT/linux-desktop" fi + core_prime_sudo platform_bootstrap printf '\n' diff --git a/platforms/macos.sh b/platforms/macos.sh index 67232bd..319f0b7 100755 --- a/platforms/macos.sh +++ b/platforms/macos.sh @@ -69,20 +69,9 @@ mac_pipx_install_tier() { mac_install_list "$(pkg_names pipx "$1")" pipx install # Cache sudo credentials once up front. Homebrew's cask/pkg installers each shell # out to `sudo`, so without this a fresh install prompts for the password ~6 -# times. Prime the timestamp once, then refresh it in the background until this -# script exits so every later sudo call reuses it silently. +# times. mac_prime_sudo() { - if [[ "$DRY_RUN" == true ]]; then - printf ' [dry-run] sudo -v (cache credentials + background keepalive)\n' - return 0 - fi - printf '==> Caching credentials (you may be prompted for your password once)...\n' - sudo -v || return 0 - # || true: the subshell inherits set -e, and one failed refresh (timestamp - # revoked mid-run) must not silently kill the keepalive. stdout is redirected - # so a piped run (setup.sh | tee) sees EOF at exit instead of hanging on the - # fd this subshell holds for up to 60s. - ( while true; do sudo -n true || true; sleep 60; kill -0 "$$" 2>/dev/null || exit; done ) >/dev/null 2>&1 & + core_prime_sudo } print_app_store_reminders() { @@ -154,6 +143,7 @@ platform_main() { fi if [[ "$DRY_RUN" == false ]]; then + prepare_nvm_environment export NVM_DIR="$HOME/.nvm" [[ -s "$NVM_DIR/nvm.sh" ]] && \. "$NVM_DIR/nvm.sh" nvm install 'lts/*'