diff --git a/aws-lightsail/lib/common.sh b/aws-lightsail/lib/common.sh index 7ee2c2a9..01d4e640 100644 --- a/aws-lightsail/lib/common.sh +++ b/aws-lightsail/lib/common.sh @@ -64,16 +64,7 @@ ensure_ssh_key() { } get_server_name() { - if [[ -n "${LIGHTSAIL_SERVER_NAME:-}" ]]; then - log_info "Using instance name from environment: $LIGHTSAIL_SERVER_NAME" - echo "$LIGHTSAIL_SERVER_NAME"; return 0 - fi - local server_name=$(safe_read "Enter Lightsail instance name: ") - if [[ -z "$server_name" ]]; then - log_error "Instance name is required" - log_warn "Set LIGHTSAIL_SERVER_NAME environment variable for non-interactive usage"; return 1 - fi - echo "$server_name" + get_resource_name "LIGHTSAIL_SERVER_NAME" "Enter Lightsail instance name: " } get_cloud_init_userdata() { diff --git a/digitalocean/lib/common.sh b/digitalocean/lib/common.sh index 830afa29..d9dd0cb3 100755 --- a/digitalocean/lib/common.sh +++ b/digitalocean/lib/common.sh @@ -62,12 +62,8 @@ ensure_do_token() { echo -e "${YELLOW}Get your token from: https://cloud.digitalocean.com/account/api/tokens${NC}" echo "" - local token=$(safe_read "Enter your DigitalOcean API token: ") || return 1 - if [[ -z "$token" ]]; then - log_error "API token cannot be empty" - log_warn "For non-interactive usage, set: DO_API_TOKEN=your-token" - return 1 - fi + local token + token=$(validated_read "Enter your DigitalOcean API token: " validate_api_token) || return 1 # Validate token export DO_API_TOKEN="$token" @@ -143,22 +139,8 @@ ensure_ssh_key() { # Get server name from env var or prompt get_server_name() { - if [[ -n "${DO_DROPLET_NAME:-}" ]]; then - log_info "Using droplet name from environment: $DO_DROPLET_NAME" - if ! validate_server_name "$DO_DROPLET_NAME"; then - return 1 - fi - echo "$DO_DROPLET_NAME" - return 0 - fi - - local server_name=$(safe_read "Enter droplet name: ") - if [[ -z "$server_name" ]]; then - log_error "Droplet name is required" - log_warn "Set DO_DROPLET_NAME environment variable for non-interactive usage:" - log_warn " DO_DROPLET_NAME=dev-mk1 curl ... | bash" - return 1 - fi + local server_name + server_name=$(get_resource_name "DO_DROPLET_NAME" "Enter droplet name: ") || return 1 if ! validate_server_name "$server_name"; then return 1 diff --git a/e2b/lib/common.sh b/e2b/lib/common.sh index 802c4f52..0bce0445 100644 --- a/e2b/lib/common.sh +++ b/e2b/lib/common.sh @@ -64,16 +64,7 @@ EOF } get_server_name() { - if [[ -n "$E2B_SANDBOX_NAME" ]]; then - log_info "Using sandbox name from environment: $E2B_SANDBOX_NAME" - echo "$E2B_SANDBOX_NAME"; return 0 - fi - local name=$(safe_read "Enter sandbox name: ") - if [[ -z "$name" ]]; then - log_error "Sandbox name is required" - log_warn "Set E2B_SANDBOX_NAME environment variable for non-interactive usage"; return 1 - fi - echo "$name" + get_resource_name "E2B_SANDBOX_NAME" "Enter sandbox name: " } create_server() { diff --git a/gcp/lib/common.sh b/gcp/lib/common.sh index cf62aff3..3d5647a0 100644 --- a/gcp/lib/common.sh +++ b/gcp/lib/common.sh @@ -57,16 +57,7 @@ ensure_ssh_key() { } get_server_name() { - if [[ -n "${GCP_INSTANCE_NAME:-}" ]]; then - log_info "Using instance name from environment: $GCP_INSTANCE_NAME" - echo "$GCP_INSTANCE_NAME"; return 0 - fi - local server_name=$(safe_read "Enter instance name: ") - if [[ -z "$server_name" ]]; then - log_error "Instance name is required" - log_warn "Set GCP_INSTANCE_NAME environment variable for non-interactive usage"; return 1 - fi - echo "$server_name" + get_resource_name "GCP_INSTANCE_NAME" "Enter instance name: " } get_cloud_init_userdata() { diff --git a/hetzner/lib/common.sh b/hetzner/lib/common.sh index 556ce7dc..0e8946bc 100755 --- a/hetzner/lib/common.sh +++ b/hetzner/lib/common.sh @@ -60,12 +60,8 @@ ensure_hcloud_token() { echo -e "${YELLOW}Get your token from: https://console.hetzner.cloud/projects → API Tokens${NC}" echo "" - local token=$(safe_read "Enter your Hetzner API token: ") || return 1 - if [[ -z "$token" ]]; then - log_error "API token cannot be empty" - log_warn "For non-interactive usage, set: HCLOUD_TOKEN=your-token" - return 1 - fi + local token + token=$(validated_read "Enter your Hetzner API token: " validate_api_token) || return 1 # Validate token by making a test API call export HCLOUD_TOKEN="$token" @@ -139,22 +135,8 @@ ensure_ssh_key() { # Get server name from env var or prompt get_server_name() { - if [[ -n "${HETZNER_SERVER_NAME:-}" ]]; then - log_info "Using server name from environment: $HETZNER_SERVER_NAME" - if ! validate_server_name "$HETZNER_SERVER_NAME"; then - return 1 - fi - echo "$HETZNER_SERVER_NAME" - return 0 - fi - - local server_name=$(safe_read "Enter server name: ") - if [[ -z "$server_name" ]]; then - log_error "Server name is required" - log_warn "Set HETZNER_SERVER_NAME environment variable for non-interactive usage:" - log_warn " HETZNER_SERVER_NAME=dev-mk1 curl ... | bash" - return 1 - fi + local server_name + server_name=$(get_resource_name "HETZNER_SERVER_NAME" "Enter server name: ") || return 1 if ! validate_server_name "$server_name"; then return 1 diff --git a/linode/lib/common.sh b/linode/lib/common.sh index 33729b62..63632425 100644 --- a/linode/lib/common.sh +++ b/linode/lib/common.sh @@ -47,12 +47,8 @@ ensure_linode_token() { fi echo ""; log_warn "Linode API Token Required" echo -e "${YELLOW}Get your token from: https://cloud.linode.com/profile/tokens${NC}"; echo "" - local token=$(safe_read "Enter your Linode API token: ") || return 1 - if [[ -z "$token" ]]; then - log_error "API token cannot be empty" - log_warn "For non-interactive usage, set: LINODE_API_TOKEN=your-token" - return 1 - fi + local token + token=$(validated_read "Enter your Linode API token: " validate_api_token) || return 1 export LINODE_API_TOKEN="$token" local response=$(linode_api GET "/profile") if echo "$response" | grep -q '"username"'; then @@ -113,21 +109,13 @@ ensure_ssh_key() { } get_server_name() { - if [[ -n "${LINODE_SERVER_NAME:-}" ]]; then - log_info "Using server name from environment: $LINODE_SERVER_NAME" - if ! validate_server_name "$LINODE_SERVER_NAME"; then - return 1 - fi - echo "$LINODE_SERVER_NAME"; return 0 - fi - local server_name=$(safe_read "Enter Linode label: ") - if [[ -z "$server_name" ]]; then - log_error "Server name is required" - log_warn "Set LINODE_SERVER_NAME environment variable for non-interactive usage"; return 1 - fi + local server_name + server_name=$(get_resource_name "LINODE_SERVER_NAME" "Enter Linode label: ") || return 1 + if ! validate_server_name "$server_name"; then return 1 fi + echo "$server_name" } diff --git a/modal/lib/common.sh b/modal/lib/common.sh index e0999303..301b1636 100644 --- a/modal/lib/common.sh +++ b/modal/lib/common.sh @@ -42,16 +42,7 @@ ensure_modal_cli() { } get_server_name() { - if [[ -n "${MODAL_SANDBOX_NAME:-}" ]]; then - log_info "Using sandbox name from environment: $MODAL_SANDBOX_NAME" - echo "$MODAL_SANDBOX_NAME"; return 0 - fi - local name=$(safe_read "Enter sandbox name: ") - if [[ -z "$name" ]]; then - log_error "Sandbox name is required" - log_warn "Set MODAL_SANDBOX_NAME environment variable for non-interactive usage"; return 1 - fi - echo "$name" + get_resource_name "MODAL_SANDBOX_NAME" "Enter sandbox name: " } create_server() { diff --git a/shared/common.sh b/shared/common.sh index 68b85799..451024fe 100644 --- a/shared/common.sh +++ b/shared/common.sh @@ -164,6 +164,114 @@ validate_server_name() { return 0 } +# Validate API token to prevent command injection +# Allows alphanumeric, dashes, underscores, and common token separators +# Blocks shell metacharacters: ; ' " < > | & $ ` \ ( ) +validate_api_token() { + local token="$1" + + if [[ -z "$token" ]]; then + log_error "API token cannot be empty" + return 1 + fi + + # Block shell metacharacters that could enable command injection + if [[ "$token" =~ [\;\'\"\<\>\|\&\$\`\\\(\)] ]]; then + log_error "Invalid token format: contains shell metacharacters" + log_error "Tokens should not contain: ; ' \" < > | & \$ \` \\ ( )" + return 1 + fi + + return 0 +} + +# Validate region/location name (cloud provider regions, datacenters, zones) +# Alphanumeric, hyphens, underscores only, 1-63 chars +validate_region_name() { + local region="$1" + + if [[ -z "$region" ]]; then + log_error "Region name cannot be empty" + return 1 + fi + + if [[ ! "$region" =~ ^[a-zA-Z0-9_-]{1,63}$ ]]; then + log_error "Invalid region name: '$region'" + log_error "Region names must be 1-63 characters: alphanumeric, hyphens, underscores only" + return 1 + fi + + return 0 +} + +# Validate resource name (generic: server types, sizes, plans, etc.) +# Alphanumeric, hyphens, underscores, dots, 1-63 chars +validate_resource_name() { + local name="$1" + + if [[ -z "$name" ]]; then + log_error "Resource name cannot be empty" + return 1 + fi + + if [[ ! "$name" =~ ^[a-zA-Z0-9_.-]{1,63}$ ]]; then + log_error "Invalid resource name: '$name'" + log_error "Resource names must be 1-63 characters: alphanumeric, hyphens, underscores, dots only" + return 1 + fi + + return 0 +} + +# Validated read wrapper - reads input and validates it with a validator function +# Usage: validated_read "prompt" validator_function_name +# Returns: Validated input via stdout, or exits on error/empty input +# Example: api_key=$(validated_read "Enter API key: " validate_api_token) +validated_read() { + local prompt="$1" + local validator="$2" + local value + + while true; do + value=$(safe_read "$prompt") || return 1 + + if [[ -z "$value" ]]; then + return 1 + fi + + if "$validator" "$value"; then + echo "$value" + return 0 + fi + + log_warn "Invalid input. Please try again." + done +} + +# Generic function to get resource name from environment or prompt +# Usage: get_resource_name ENV_VAR_NAME PROMPT_TEXT +# Returns: Resource name via stdout +# Example: get_resource_name "LIGHTSAIL_SERVER_NAME" "Enter Lightsail instance name: " +get_resource_name() { + local env_var_name="$1" + local prompt_text="$2" + local resource_value="${!env_var_name}" + + if [[ -n "$resource_value" ]]; then + log_info "Using ${prompt_text%:*} from environment: $resource_value" + echo "$resource_value" + return 0 + fi + + local name + name=$(safe_read "$prompt_text") + if [[ -z "$name" ]]; then + log_error "${prompt_text%:*} is required" + log_warn "Set $env_var_name environment variable for non-interactive usage" + return 1 + fi + echo "$name" +} # Interactively prompt for model ID with validation # Usage: get_model_id_interactive [default_model] [agent_name] # Returns: Model ID via stdout diff --git a/sprite/lib/common.sh b/sprite/lib/common.sh index c1bf8808..411358bf 100644 --- a/sprite/lib/common.sh +++ b/sprite/lib/common.sh @@ -29,24 +29,8 @@ ensure_sprite_authenticated() { # Prompt for sprite name get_sprite_name() { - # Check if SPRITE_NAME is already set in environment - if [[ -n "${SPRITE_NAME:-}" ]]; then - log_info "Using sprite name from environment: $SPRITE_NAME" - if ! validate_server_name "$SPRITE_NAME"; then - return 1 - fi - echo "$SPRITE_NAME" - return 0 - fi - - # Try to read interactively - local sprite_name=$(safe_read "Enter sprite name: ") - if [[ -z "$sprite_name" ]]; then - log_error "Sprite name is required" - log_warn "Set SPRITE_NAME environment variable for non-interactive usage:" - log_warn " SPRITE_NAME=dev-mk1 curl ... | bash" - return 1 - fi + local sprite_name + sprite_name=$(get_resource_name "SPRITE_NAME" "Enter sprite name: ") || return 1 if ! validate_server_name "$sprite_name"; then return 1 diff --git a/vultr/lib/common.sh b/vultr/lib/common.sh index 692ec06a..e42fda52 100755 --- a/vultr/lib/common.sh +++ b/vultr/lib/common.sh @@ -54,12 +54,8 @@ ensure_vultr_token() { log_warn "Vultr API Key Required" echo -e "${YELLOW}Get your API key from: https://my.vultr.com/settings/#settingsapi${NC}" echo "" - local api_key=$(safe_read "Enter your Vultr API key: ") || return 1 - if [[ -z "$api_key" ]]; then - log_error "API key cannot be empty" - log_warn "For non-interactive usage, set: VULTR_API_KEY=your-key" - return 1 - fi + local api_key + api_key=$(validated_read "Enter your Vultr API key: " validate_api_token) || return 1 export VULTR_API_KEY="$api_key" local response=$(vultr_api GET "/account") if echo "$response" | grep -q '"account"'; then @@ -129,23 +125,13 @@ ensure_ssh_key() { } get_server_name() { - if [[ -n "${VULTR_SERVER_NAME:-}" ]]; then - log_info "Using server name from environment: $VULTR_SERVER_NAME" - if ! validate_server_name "$VULTR_SERVER_NAME"; then - return 1 - fi - echo "$VULTR_SERVER_NAME" - return 0 - fi - local server_name=$(safe_read "Enter server name: ") - if [[ -z "$server_name" ]]; then - log_error "Server name is required" - log_warn "Set VULTR_SERVER_NAME environment variable for non-interactive usage" - return 1 - fi + local server_name + server_name=$(get_resource_name "VULTR_SERVER_NAME" "Enter server name: ") || return 1 + if ! validate_server_name "$server_name"; then return 1 fi + echo "$server_name" }