mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-22 11:24:18 +00:00
- Fix triple-quote injection in SSH keys (Scaleway, UpCloud), userdata (BinaryLane), init scripts (Civo, Kamatera), and GraphQL queries (RunPod) by passing data via stdin/json_escape instead of inline string interpolation - Add input validation for all cloud provider env vars (region, type, plan, etc.) using validate_region_name/validate_resource_name to block shell metacharacters before they reach Python string interpolation - Validate Modal image name as Python identifier to prevent code injection - Validate numeric env vars (RAM, GPU count, disk size) across all providers Affects: 19 cloud provider lib/common.sh files Agent: security-auditor Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
179 lines
6.5 KiB
Bash
179 lines
6.5 KiB
Bash
#!/bin/bash
|
|
# Common bash functions for AWS Lightsail spawn scripts
|
|
# Uses AWS CLI (aws lightsail) — requires `aws` CLI configured with credentials
|
|
|
|
# Bash safety flags
|
|
set -eo pipefail
|
|
|
|
# ============================================================
|
|
# Provider-agnostic functions
|
|
# ============================================================
|
|
|
|
# Source shared provider-agnostic functions (local or remote fallback)
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/../../shared/common.sh" ]]; then
|
|
source "${SCRIPT_DIR}/../../shared/common.sh"
|
|
else
|
|
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)"
|
|
fi
|
|
|
|
# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
|
|
|
|
# ============================================================
|
|
# AWS Lightsail specific functions
|
|
# ============================================================
|
|
|
|
# SSH_OPTS is now defined in shared/common.sh
|
|
|
|
# Configurable timeout/delay constants
|
|
INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5} # Delay between instance status checks
|
|
|
|
ensure_aws_cli() {
|
|
if ! command -v aws &>/dev/null; then
|
|
log_error "AWS CLI is required. Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
|
|
return 1
|
|
fi
|
|
# Verify credentials are configured
|
|
if ! aws sts get-caller-identity &>/dev/null; then
|
|
log_error "AWS CLI not configured. Run: aws configure"
|
|
return 1
|
|
fi
|
|
local region="${AWS_DEFAULT_REGION:-${LIGHTSAIL_REGION:-us-east-1}}"
|
|
export AWS_DEFAULT_REGION="${region}"
|
|
log_info "Using AWS region: ${region}"
|
|
}
|
|
|
|
ensure_ssh_key() {
|
|
local key_path="${HOME}/.ssh/id_ed25519"
|
|
local pub_path="${key_path}.pub"
|
|
|
|
# Generate key if needed
|
|
generate_ssh_key_if_missing "${key_path}"
|
|
|
|
local key_name="spawn-key"
|
|
|
|
# Check if already registered
|
|
if aws lightsail get-key-pair --key-pair-name "${key_name}" &>/dev/null; then
|
|
log_info "SSH key already registered with Lightsail"
|
|
return 0
|
|
fi
|
|
|
|
log_warn "Importing SSH key to Lightsail..."
|
|
aws lightsail import-key-pair \
|
|
--key-pair-name "${key_name}" \
|
|
--public-key-base64 "$(base64 -w0 "${pub_path}" 2>/dev/null || base64 "${pub_path}")" \
|
|
>/dev/null
|
|
log_info "SSH key imported to Lightsail"
|
|
}
|
|
|
|
get_server_name() {
|
|
get_resource_name "LIGHTSAIL_SERVER_NAME" "Enter Lightsail instance name: "
|
|
}
|
|
|
|
get_cloud_init_userdata() {
|
|
cat << 'CLOUD_INIT_EOF'
|
|
#!/bin/bash
|
|
apt-get update -y
|
|
apt-get install -y curl unzip git zsh
|
|
# Install Bun
|
|
su - ubuntu -c 'curl -fsSL https://bun.sh/install | bash'
|
|
# Install Claude Code
|
|
su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash'
|
|
# Configure PATH
|
|
echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.bashrc
|
|
echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.zshrc
|
|
touch /home/ubuntu/.cloud-init-complete
|
|
chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete
|
|
CLOUD_INIT_EOF
|
|
}
|
|
|
|
create_server() {
|
|
local name="${1}"
|
|
local bundle="${LIGHTSAIL_BUNDLE:-medium_3_0}"
|
|
local region="${AWS_DEFAULT_REGION:-us-east-1}"
|
|
local az="${region}a"
|
|
local blueprint="ubuntu_24_04"
|
|
|
|
# Validate env var inputs to prevent command injection
|
|
validate_resource_name "${bundle}" || { log_error "Invalid LIGHTSAIL_BUNDLE"; return 1; }
|
|
validate_region_name "${region}" || { log_error "Invalid AWS_DEFAULT_REGION"; return 1; }
|
|
|
|
log_warn "Creating Lightsail instance '${name}' (bundle: ${bundle}, AZ: ${az})..."
|
|
|
|
local userdata
|
|
userdata=$(get_cloud_init_userdata)
|
|
|
|
if ! aws lightsail create-instances \
|
|
--instance-names "${name}" \
|
|
--availability-zone "${az}" \
|
|
--blueprint-id "${blueprint}" \
|
|
--bundle-id "${bundle}" \
|
|
--key-pair-name "spawn-key" \
|
|
--user-data "${userdata}" \
|
|
>/dev/null; then
|
|
log_error "Failed to create Lightsail instance"
|
|
return 1
|
|
fi
|
|
|
|
export LIGHTSAIL_INSTANCE_NAME="${name}"
|
|
log_info "Instance creation initiated: ${name}"
|
|
|
|
# Wait for instance to become running and get IP
|
|
log_warn "Waiting for instance to become running..."
|
|
local max_attempts=60 attempt=1
|
|
while [[ ${attempt} -le ${max_attempts} ]]; do
|
|
local state
|
|
state=$(aws lightsail get-instance --instance-name "${name}" \
|
|
--query 'instance.state.name' --output text 2>/dev/null)
|
|
|
|
if [[ "${state}" == "running" ]]; then
|
|
LIGHTSAIL_SERVER_IP=$(aws lightsail get-instance --instance-name "${name}" \
|
|
--query 'instance.publicIpAddress' --output text)
|
|
export LIGHTSAIL_SERVER_IP
|
|
log_info "Instance running: IP=${LIGHTSAIL_SERVER_IP}"
|
|
return 0
|
|
fi
|
|
log_warn "Instance state: ${state} (${attempt}/${max_attempts})"
|
|
sleep "${INSTANCE_STATUS_POLL_DELAY}"; attempt=$((attempt + 1))
|
|
done
|
|
log_error "Instance did not become running in time"; return 1
|
|
}
|
|
|
|
verify_server_connectivity() {
|
|
local ip="${1}" max_attempts=${2:-30}
|
|
# Use shared generic_ssh_wait with exponential backoff
|
|
# shellcheck disable=SC2086,SC2154
|
|
generic_ssh_wait "ubuntu" "${ip}" "${SSH_OPTS}" "echo ok" "SSH connectivity" "${max_attempts}"
|
|
}
|
|
|
|
wait_for_cloud_init() {
|
|
local ip="${1}"
|
|
local max_attempts=${2:-60}
|
|
|
|
# First ensure SSH connectivity is established
|
|
# shellcheck disable=SC2086
|
|
generic_ssh_wait "ubuntu" "${ip}" "${SSH_OPTS}" "echo ok" "SSH connectivity" 30 5 || return 1
|
|
|
|
# Then wait for cloud-init completion marker
|
|
# shellcheck disable=SC2086
|
|
generic_ssh_wait "ubuntu" "${ip}" "${SSH_OPTS}" "test -f /home/ubuntu/.cloud-init-complete" "cloud-init" "${max_attempts}" 5
|
|
}
|
|
|
|
# Note: Lightsail uses 'ubuntu' user, not 'root'
|
|
# shellcheck disable=SC2086
|
|
run_server() { local ip="${1}" cmd="${2}"; ssh ${SSH_OPTS} "ubuntu@${ip}" "${cmd}"; }
|
|
# shellcheck disable=SC2086
|
|
upload_file() { local ip="${1}" local_path="${2}" remote_path="${3}"; scp ${SSH_OPTS} "${local_path}" "ubuntu@${ip}:${remote_path}"; }
|
|
# shellcheck disable=SC2086
|
|
interactive_session() { local ip="${1}" cmd="${2}"; ssh -t ${SSH_OPTS} "ubuntu@${ip}" "${cmd}"; }
|
|
|
|
destroy_server() {
|
|
local name="${1}"
|
|
log_warn "Destroying Lightsail instance ${name}..."
|
|
aws lightsail delete-instance --instance-name "${name}" >/dev/null
|
|
log_info "Instance ${name} destroyed"
|
|
}
|
|
|
|
list_servers() {
|
|
aws lightsail get-instances --query 'instances[].{Name:name,State:state.name,IP:publicIpAddress,Bundle:bundleId}' --output table
|
|
}
|