diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2aee412 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Twilio Network Traversal Service Credentials +# Get these from your Twilio Console: +# 1. Go to https://console.twilio.com/ +# 2. Navigate to Account > API Keys & Tokens +# 3. Create a new API Key +# 4. Use the SID as TWILIO_ACCOUNT_SID +# 5. Use the Secret as TWILIO_AUTH_TOKEN +TWILIO_ACCOUNT_SID=SK...your_api_key_sid_here +TWILIO_AUTH_TOKEN=your_api_key_secret_here + +# Google Cloud Configuration +# If not provided, will use current gcloud config +PROJECT_ID=your-gcp-project-id +# REGION=us-central1 + +# Optional: Service Configuration +# SERVICE_NAME=kernel-browser \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d571dba --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Environment variables +.env +.env.local +*.env +!.env.example + +# Node modules +node_modules/ + +# Build outputs +dist/ +build/ +out/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS files +.DS_Store +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# Temporary files +tmp/ +temp/ +*.tmp + +# Python +__pycache__/ +*.py[cod] +*$py.class +.Python +venv/ +env/ + +# Google Cloud +.gcloudignore +gcs-key.json +service-account-key.json + +# Docker +.dockerignore + +# Backup files +*.bak +*.backup \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..b029426 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,166 @@ +# Kernel Browser - Cloud Run Deployment Guide + +This guide explains how to deploy the Kernel Browser to Google Cloud Run with secure Twilio credential management. + +## Prerequisites + +- Google Cloud SDK (`gcloud`) installed +- Docker installed +- Git installed +- A Google Cloud Project with billing enabled +- Twilio account with API credentials (for WebRTC TURN servers) + +## Quick Start + +### 1. Clone the repository +```bash +git clone +cd browser-web-agent +git submodule update --init --recursive +``` + +### 2. Set up Twilio credentials +```bash +# Copy the example environment file +cp .env.example .env + +# Edit .env and add your Twilio credentials +# Get these from https://console.twilio.com/ > Account > API Keys & Tokens +``` + +Your `.env` file should contain: +``` +TWILIO_ACCOUNT_SID=SK...your_api_key_sid_here +TWILIO_AUTH_TOKEN=your_api_key_secret_here +``` + +### 3. Deploy to Cloud Run +```bash +./deploy.sh +``` + +The script will: +- Load credentials from `.env` +- Create/update secrets in Google Secret Manager +- Build and deploy the container to Cloud Run +- Configure all necessary permissions + +## Deployment Options + +### Using Cloud Build (recommended) +```bash +./deploy.sh +``` + +### Using local Docker build +```bash +./deploy.sh --local +``` + +### Specify project and region +```bash +./deploy.sh --project YOUR_PROJECT_ID --region us-central1 +``` + +## How It Works + +### Credential Management + +1. **Local Development**: Credentials are stored in `.env` file (gitignored) +2. **Secret Manager**: Deploy script automatically creates/updates secrets in Google Secret Manager +3. **Cloud Run**: Service uses `secretKeyRef` to securely access credentials at runtime +4. **Dynamic TURN**: Container fetches fresh TURN credentials from Twilio on startup + +### Security Features + +- Credentials never appear in code or logs +- Secrets are encrypted at rest and in transit +- Service account has minimal required permissions +- Automatic credential rotation support + +### Files Overview + +- `.env.example` - Template for environment variables +- `.env` - Your local credentials (gitignored) +- `deploy.sh` - Main deployment script with Secret Manager integration +- `service-secrets.yaml` - Cloud Run config with secret references +- `service.yaml` - Fallback config (for deployments without secrets) +- `cloudbuild.yaml` - Cloud Build configuration +- `twilio/` - Twilio credential management scripts + +## Updating Credentials + +To update Twilio credentials: + +1. Update `.env` with new credentials +2. Run `./deploy.sh` again +3. Script will update secrets and redeploy + +## Manual Secret Management + +If you need to manage secrets manually: + +```bash +# Create secrets +echo -n "YOUR_SID" | gcloud secrets create twilio-account-sid --data-file=- +echo -n "YOUR_TOKEN" | gcloud secrets create twilio-auth-token --data-file=- + +# Update secrets +echo -n "NEW_SID" | gcloud secrets versions add twilio-account-sid --data-file=- +echo -n "NEW_TOKEN" | gcloud secrets versions add twilio-auth-token --data-file=- + +# Grant access to service account +gcloud secrets add-iam-policy-binding twilio-account-sid \ + --member="serviceAccount:kernel-browser-sa@PROJECT_ID.iam.gserviceaccount.com" \ + --role="roles/secretmanager.secretAccessor" +``` + +## Service Endpoints + +After deployment, you'll have access to: + +- **Main Interface**: `https://SERVICE_URL/` +- **WebRTC Client**: `https://SERVICE_URL/` +- **Chrome DevTools**: `https://SERVICE_URL/devtools/` +- **DevTools WebSocket**: `wss://SERVICE_URL/cdp/ws` +- **Recording API**: `https://SERVICE_URL/api` +- **Health Check**: `https://SERVICE_URL/health` + +## Troubleshooting + +### Deployment fails +- Check that all prerequisites are installed +- Ensure billing is enabled on your GCP project +- Verify you have sufficient quota in your region + +### WebRTC not working +- Ensure Twilio credentials are correct +- Check Cloud Run logs: `gcloud run services logs read kernel-browser --region=us-central1` +- Verify TURN servers are accessible from your network + +### Secrets not found +- Run `gcloud secrets list` to verify secrets exist +- Check service account permissions +- Ensure Secret Manager API is enabled + +## Architecture + +``` +┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Client │────▶│ Cloud Run │────▶│ Secret Manager │ +│ (Browser) │ │ (Container) │ │ (Credentials) │ +└─────────────┘ └──────────────────┘ └─────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Twilio API │ + │ (TURN Servers) │ + └──────────────────┘ +``` + +## Support + +For issues or questions: +- Check logs: `gcloud run services logs read kernel-browser --region=us-central1` +- Review service status: `gcloud run services describe kernel-browser --region=us-central1` +- File an issue on GitHub \ No newline at end of file diff --git a/Dockerfile.cloudrun b/Dockerfile.cloudrun index 8c3e812..91ca6c7 100644 --- a/Dockerfile.cloudrun +++ b/Dockerfile.cloudrun @@ -1,3 +1,57 @@ +# DevTools Frontend build stage using browser-operator-core +FROM --platform=linux/amd64 ubuntu:22.04 AS devtools-builder + +# Cache bust argument to force rebuilds +ARG CACHE_BUST + +# Install required packages for DevTools frontend build +RUN apt-get update && apt-get install -y \ + curl \ + git \ + python3 \ + python3-pip \ + python-is-python3 \ + wget \ + unzip \ + sudo \ + ca-certificates \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js 18.x +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +# Clone depot_tools +RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +ENV PATH="/workspace/depot_tools:${PATH}" +ENV DEPOT_TOOLS_UPDATE=0 + +# Follow README instructions exactly - fetching code +RUN mkdir devtools +WORKDIR /workspace/devtools +RUN fetch devtools-frontend + +# Build steps +WORKDIR /workspace/devtools/devtools-frontend + +RUN gclient sync +RUN /workspace/depot_tools/ensure_bootstrap + +# Build standard DevTools first +RUN npm run build + +# Add Browser Operator fork and switch to it +RUN git remote add upstream https://github.com/BrowserOperator/browser-operator-core.git +RUN git fetch upstream +RUN git checkout upstream/main + +# Build Browser Operator version +RUN npm run build + # Multi-stage build using kernel-images as base FROM docker.io/golang:1.25.0 AS server-builder WORKDIR /workspace/server @@ -90,6 +144,12 @@ RUN apt-get update && \ nginx \ # PPA req software-properties-common && \ + # Disable nginx auto-start to prevent conflicts with custom config + systemctl disable nginx || true && \ + systemctl mask nginx || true && \ + # Remove default nginx config to prevent conflicts + rm -f /etc/nginx/sites-enabled/default && \ + rm -f /etc/nginx/nginx.conf && \ # Userland apps sudo add-apt-repository ppa:mozillateam/ppa && \ sudo apt-get install -y --no-install-recommends \ @@ -186,19 +246,40 @@ COPY kernel-images/images/chromium-headful/supervisor/services/ /etc/supervisor/ # Copy the kernel-images API binary COPY --from=server-builder /out/kernel-images-api /usr/local/bin/kernel-images-api -# Cloud Run specific: nginx configuration for port proxying -COPY nginx.conf /etc/nginx/nginx.conf +# ============================================================================ +# DevTools Integration +# ============================================================================ + +# Copy DevTools static files from builder +COPY --from=devtools-builder /workspace/devtools/devtools-frontend/out/Default/gen/front_end /usr/share/nginx/devtools + +# Set permissions for DevTools files +RUN chown -R kernel:kernel /usr/share/nginx/devtools + +# Cloud Run specific: wrapper scripts (nginx config is inline) +# DO NOT copy nginx.conf to avoid auto-start conflicts COPY cloudrun-wrapper.sh /cloudrun-wrapper.sh -RUN chmod +x /cloudrun-wrapper.sh +COPY twilio/twilio-credential-updater.sh /twilio-credential-updater.sh +RUN chmod +x /cloudrun-wrapper.sh /twilio-credential-updater.sh + +# Add essential services for neko WebRTC and Chromium +COPY supervisor/services-cloudrun/dbus.conf /etc/supervisor/conf.d/services-cloudrun/dbus.conf +COPY supervisor/services-cloudrun/xorg.conf /etc/supervisor/conf.d/services-cloudrun/xorg.conf +COPY supervisor/services-cloudrun/neko.conf /etc/supervisor/conf.d/services-cloudrun/neko.conf +COPY supervisor/services-cloudrun/chromium.conf /etc/supervisor/conf.d/services-cloudrun/chromium.conf +COPY supervisor/services-cloudrun/devtools-frontend.conf /etc/supervisor/conf.d/services-cloudrun/devtools-frontend.conf # Create nginx temp directories for non-root execution RUN mkdir -p /tmp/nginx_client_temp /tmp/nginx_proxy_temp /tmp/nginx_fastcgi_temp \ - /tmp/nginx_uwsgi_temp /tmp/nginx_scgi_temp && \ + /tmp/nginx_uwsgi_temp /tmp/nginx_scgi_temp \ + /tmp/nginx_devtools_client_temp /tmp/nginx_devtools_proxy_temp /tmp/nginx_devtools_fastcgi_temp \ + /tmp/nginx_devtools_uwsgi_temp /tmp/nginx_devtools_scgi_temp && \ chown -R kernel:kernel /tmp/nginx_* # Create supervisor log directories RUN mkdir -p /var/log/supervisord/chromium /var/log/supervisord/neko /var/log/supervisord/xorg \ - /var/log/supervisord/dbus /var/log/supervisord/kernel-images-api /var/log/supervisord/mutter && \ + /var/log/supervisord/dbus /var/log/supervisord/kernel-images-api /var/log/supervisord/mutter \ + /var/log/supervisord/nginx /var/log/supervisord/devtools-frontend && \ chown -R kernel:kernel /var/log/supervisord # Create health check endpoint diff --git a/Dockerfile.devtools b/Dockerfile.devtools new file mode 100644 index 0000000..edefa5e --- /dev/null +++ b/Dockerfile.devtools @@ -0,0 +1,63 @@ +# DevTools Frontend build stage using browser-operator-core +FROM --platform=linux/amd64 ubuntu:22.04 AS devtools-builder + +# Install required packages for DevTools frontend build +RUN apt-get update && apt-get install -y \ + curl \ + git \ + python3 \ + python3-pip \ + python-is-python3 \ + wget \ + unzip \ + sudo \ + ca-certificates \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js 18.x +RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -y nodejs && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace + +# Clone depot_tools +RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git +ENV PATH="/workspace/depot_tools:${PATH}" +ENV DEPOT_TOOLS_UPDATE=0 + +# Follow README instructions exactly - fetching code +RUN mkdir devtools +WORKDIR /workspace/devtools +RUN fetch devtools-frontend + +# Build steps +WORKDIR /workspace/devtools/devtools-frontend + +RUN gclient sync +RUN /workspace/depot_tools/ensure_bootstrap + +# Build standard DevTools first +RUN npm run build + +# Add Browser Operator fork and switch to it +RUN git remote add upstream https://github.com/BrowserOperator/browser-operator-core.git +RUN git fetch upstream +RUN git checkout upstream/main + +# Build Browser Operator version +RUN npm run build + +# Production stage for DevTools frontend +FROM nginx:alpine AS devtools-frontend +WORKDIR /usr/share/nginx/html + +# Copy the built DevTools frontend from builder +COPY --from=devtools-builder /workspace/devtools/devtools-frontend/out/Default/gen/front_end . + +# Copy nginx config from browser-operator-core +COPY browser-operator-core/docker/nginx.conf /etc/nginx/conf.d/default.conf + +# Create health check endpoint +RUN echo '{"status": "healthy", "service": "browser-operator-devtools"}' > health.json \ No newline at end of file diff --git a/build-local.sh b/build-local.sh new file mode 100755 index 0000000..b388617 --- /dev/null +++ b/build-local.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +# Extended local build wrapper for kernel-browser with DevTools +set -e -o pipefail + +echo "🔨 Building extended kernel-browser with DevTools frontend..." + +# Ensure we're in the right directory +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +cd "$SCRIPT_DIR" + +# Check if kernel-images submodule exists and is initialized +if [ ! -d "kernel-images" ]; then + echo "❌ Error: kernel-images submodule not found" + echo " Run: git submodule update --init --recursive" + exit 1 +fi + +if [ ! -f "kernel-images/images/chromium-headful/build-docker.sh" ]; then + echo "❌ Error: kernel-images submodule appears empty" + echo " Run: git submodule update --init --recursive" + exit 1 +fi + +echo "🚀 Starting extended build with Docker..." +echo " Using: Dockerfile.local" +echo " Target image: kernel-browser:extended" + +# Build using Docker with extended Dockerfile +docker build -f Dockerfile.local -t kernel-browser:extended . + +echo "✅ Extended build completed successfully!" +echo " Image built: kernel-browser:extended" +echo " Includes: Chromium + DevTools frontend + WebRTC" +echo "" +echo "🏃 To run locally, use: ./run-local.sh" \ No newline at end of file diff --git a/cloudbuild.yaml b/cloudbuild.yaml index 37dac28..4d2bfe2 100644 --- a/cloudbuild.yaml +++ b/cloudbuild.yaml @@ -18,47 +18,57 @@ steps: - '-c' - | echo "Attempting to pull previous image for caching..." - docker pull gcr.io/$PROJECT_ID/kernel-browser:latest || echo "No previous image found for caching" + docker pull us-docker.pkg.dev/$PROJECT_ID/gcr.io/kernel-browser:latest || echo "No previous image found for caching" - # Step 3: Build the Docker image with caching (using kernel-cloud Dockerfile) + # Step 3: Build the Docker image with caching (using cloudrun Dockerfile) - name: 'gcr.io/cloud-builders/docker' args: - 'build' - '--file' - - 'Dockerfile.kernel-cloud' + - 'Dockerfile.cloudrun' - '--cache-from' - - 'gcr.io/$PROJECT_ID/kernel-browser:latest' + - 'us-docker.pkg.dev/$PROJECT_ID/gcr.io/kernel-browser:latest' + - '--build-arg' + - 'CACHE_BUST=$BUILD_ID' - '--tag' - - 'gcr.io/$PROJECT_ID/kernel-browser:latest' + - 'us-docker.pkg.dev/$PROJECT_ID/gcr.io/kernel-browser:latest' - '.' timeout: '3600s' # Allow 1 hour for build (it's a large image) - # Step 4: Push the image to Google Container Registry + # Step 4: Push the image to Artifact Registry - name: 'gcr.io/cloud-builders/docker' args: - 'push' - - 'gcr.io/$PROJECT_ID/kernel-browser:latest' + - 'us-docker.pkg.dev/$PROJECT_ID/gcr.io/kernel-browser:latest' - # Step 5: Update the service.yaml with the correct project ID + # Step 5: Deploy to Cloud Run with appropriate service.yaml - name: 'gcr.io/cloud-builders/gcloud' entrypoint: 'bash' args: - '-c' - | - sed -i "s/PROJECT_ID/$PROJECT_ID/g" service.yaml - cat service.yaml + # Check if Twilio secrets exist and choose appropriate service file + if gcloud secrets describe twilio-account-sid --project=$PROJECT_ID >/dev/null 2>&1 && \ + gcloud secrets describe twilio-auth-token --project=$PROJECT_ID >/dev/null 2>&1; then + echo "Using service-secrets.yaml with Secret Manager references" + cp service-secrets.yaml temp-service.yaml + else + echo "Using standard service.yaml (secrets not configured)" + cp service.yaml temp-service.yaml + fi + + # Update project ID in the chosen service file + sed -i "s/PROJECT_ID/$PROJECT_ID/g" temp-service.yaml + + echo "Deploying service configuration:" + cat temp-service.yaml + + # Deploy to Cloud Run + gcloud run services replace temp-service.yaml \ + --region=us-central1 \ + --quiet - # Step 6: Deploy to Cloud Run - - name: 'gcr.io/cloud-builders/gcloud' - args: - - 'run' - - 'services' - - 'replace' - - 'service.yaml' - - '--region=us-central1' - - '--quiet' - - # Step 7: Update traffic to latest revision + # Step 6: Update traffic to latest revision - name: 'gcr.io/cloud-builders/gcloud' args: - 'run' @@ -69,7 +79,7 @@ steps: - '--region=us-central1' - '--quiet' - # Step 8: Get the service URL + # Step 7: Get the service URL - name: 'gcr.io/cloud-builders/gcloud' args: - 'run' @@ -89,9 +99,9 @@ options: # Allocate disk space for the large build diskSizeGb: 100 -# Images to be pushed to Container Registry +# Images to be pushed to Artifact Registry images: - - 'gcr.io/$PROJECT_ID/kernel-browser:latest' + - 'us-docker.pkg.dev/$PROJECT_ID/gcr.io/kernel-browser:latest' # Tags for organization diff --git a/cloudrun-wrapper.sh b/cloudrun-wrapper.sh index a62fdf6..c49c995 100644 --- a/cloudrun-wrapper.sh +++ b/cloudrun-wrapper.sh @@ -11,40 +11,187 @@ export ENABLE_WEBRTC=true export DISPLAY_NUM=1 export HEIGHT=768 export WIDTH=1024 +export NEKO_BIND=:8081 + +# Get fresh Twilio TURN credentials if available +if [ -f /twilio-credential-updater.sh ]; then + echo "[cloudrun-wrapper] Getting fresh Twilio TURN credentials..." + source /twilio-credential-updater.sh +else + echo "[cloudrun-wrapper] Twilio updater not found, using credentials from environment" +fi # Port configuration for Cloud Run export PORT=${PORT:-8080} -export CHROMIUM_FLAGS="${CHROMIUM_FLAGS:---user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --no-sandbox --disable-setuid-sandbox --disable-features=VizDisplayCompositor}" +export CHROMIUM_FLAGS="${CHROMIUM_FLAGS:---user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --no-sandbox --disable-setuid-sandbox --disable-features=VizDisplayCompositor --custom-devtools-frontend=http://localhost:8001/ https://www.google.com}" # Setup directories with proper permissions mkdir -p /tmp/nginx_client_temp /tmp/nginx_proxy_temp /tmp/nginx_fastcgi_temp \ /tmp/nginx_uwsgi_temp /tmp/nginx_scgi_temp \ + /tmp/nginx_devtools_client_temp /tmp/nginx_devtools_proxy_temp /tmp/nginx_devtools_fastcgi_temp \ + /tmp/nginx_devtools_uwsgi_temp /tmp/nginx_devtools_scgi_temp \ /home/kernel/user-data /home/kernel/.config /home/kernel/.cache \ - /tmp/runtime-kernel /var/log/neko /tmp/recordings + /tmp/runtime-kernel /var/log/neko /tmp/recordings \ + /tmp/supervisord /tmp/dbus -# Test nginx configuration -echo "[cloudrun-wrapper] Testing nginx configuration..." -if ! nginx -t; then - echo "[cloudrun-wrapper] ERROR: nginx configuration test failed" - exit 1 -fi +# Start nginx immediately in background to respond to CloudRun health checks +echo "[cloudrun-wrapper] Starting nginx proxy on port $PORT (background)" + +# Create nginx config file +cat > /tmp/nginx.conf </dev/null || true - supervisorctl -c /etc/supervisor/supervisord-cloudrun.conf stop all 2>/dev/null || true -} -trap cleanup TERM INT +# Wait for neko service (port 8081) to be ready +for i in {1..60}; do + if curl -s http://127.0.0.1:8081/ > /dev/null 2>&1; then + echo "[cloudrun-wrapper] Neko service is ready" + break + fi + if [ $i -eq 60 ]; then + echo "[cloudrun-wrapper] Warning: Neko service not ready after 60 seconds, starting nginx anyway" + fi + sleep 1 +done -# Start nginx in foreground (main process for Cloud Run) +# Start nginx in foreground (required for Cloud Run) echo "[cloudrun-wrapper] Starting nginx proxy on port $PORT" -nginx -g "daemon off;" \ No newline at end of file +exec nginx -g "daemon off;" -c /tmp/nginx.conf \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 3d7f4b1..2996275 100755 --- a/deploy.sh +++ b/deploy.sh @@ -34,6 +34,127 @@ info() { echo -e "ℹ️ $1" } +# Load environment variables from .env file +load_env_file() { + if [ -f .env ]; then + info "Loading configuration from .env file..." + # Export variables from .env, ignoring comments and empty lines + set -a + . .env + set +a + success "Configuration loaded from .env" + elif [ -f .env.example ]; then + warning "No .env file found. Copy .env.example to .env and add your credentials" + info "Run: cp .env.example .env" + fi +} + +# Validate Twilio credentials +validate_twilio_credentials() { + if [ -z "${TWILIO_ACCOUNT_SID:-}" ] || [ -z "${TWILIO_AUTH_TOKEN:-}" ]; then + warning "Twilio credentials not found in environment" + echo "You can either:" + echo " 1. Add them to .env file (recommended)" + echo " 2. Enter them now (temporary)" + echo " 3. Skip (WebRTC may not work properly)" + echo + read -p "Enter choice [1/2/3]: " choice + + case $choice in + 1) + info "Please add credentials to .env file and re-run the script" + exit 0 + ;; + 2) + read -p "Enter Twilio Account SID: " TWILIO_ACCOUNT_SID + read -s -p "Enter Twilio Auth Token: " TWILIO_AUTH_TOKEN + echo + ;; + 3) + warning "Skipping Twilio configuration" + return 1 + ;; + *) + error "Invalid choice" + ;; + esac + fi + + if [ -n "${TWILIO_ACCOUNT_SID:-}" ] && [ -n "${TWILIO_AUTH_TOKEN:-}" ]; then + # Basic validation of credential format + if [[ ! "$TWILIO_ACCOUNT_SID" =~ ^SK[a-f0-9]{32}$ ]]; then + warning "Twilio Account SID format looks incorrect (should start with SK and be 34 chars)" + fi + success "Twilio credentials configured" + return 0 + else + return 1 + fi +} + +# Setup Google Secret Manager secrets +setup_secrets() { + if ! validate_twilio_credentials; then + warning "Skipping Secret Manager setup - no Twilio credentials provided" + return 0 + fi + + info "Setting up Google Secret Manager secrets..." + + # Enable Secret Manager API if not already enabled + info "Enabling Secret Manager API..." + gcloud services enable secretmanager.googleapis.com --project="$PROJECT_ID" --quiet + + # Create or update twilio-account-sid secret + if gcloud secrets describe twilio-account-sid --project="$PROJECT_ID" &>/dev/null; then + echo -n "$TWILIO_ACCOUNT_SID" | gcloud secrets versions add twilio-account-sid \ + --data-file=- \ + --project="$PROJECT_ID" + info "Updated twilio-account-sid secret" + else + echo -n "$TWILIO_ACCOUNT_SID" | gcloud secrets create twilio-account-sid \ + --data-file=- \ + --project="$PROJECT_ID" \ + --replication-policy="automatic" + success "Created twilio-account-sid secret" + fi + + # Create or update twilio-auth-token secret + if gcloud secrets describe twilio-auth-token --project="$PROJECT_ID" &>/dev/null; then + echo -n "$TWILIO_AUTH_TOKEN" | gcloud secrets versions add twilio-auth-token \ + --data-file=- \ + --project="$PROJECT_ID" + info "Updated twilio-auth-token secret" + else + echo -n "$TWILIO_AUTH_TOKEN" | gcloud secrets create twilio-auth-token \ + --data-file=- \ + --project="$PROJECT_ID" \ + --replication-policy="automatic" + success "Created twilio-auth-token secret" + fi + + # Grant access to service account + local sa_email="kernel-browser-sa@${PROJECT_ID}.iam.gserviceaccount.com" + + info "Granting Secret Manager access to service account..." + gcloud secrets add-iam-policy-binding twilio-account-sid \ + --member="serviceAccount:$sa_email" \ + --role="roles/secretmanager.secretAccessor" \ + --project="$PROJECT_ID" \ + --quiet + + gcloud secrets add-iam-policy-binding twilio-auth-token \ + --member="serviceAccount:$sa_email" \ + --role="roles/secretmanager.secretAccessor" \ + --project="$PROJECT_ID" \ + --quiet + + # Set flag to use secrets-enabled service.yaml + export USE_SECRETS=true + + success "Secret Manager configured with Twilio credentials" +} + # Check prerequisites check_prerequisites() { info "Checking prerequisites..." @@ -85,6 +206,7 @@ enable_apis() { "containerregistry.googleapis.com" "compute.googleapis.com" "storage.googleapis.com" + "secretmanager.googleapis.com" ) for api in "${apis[@]}"; do @@ -175,16 +297,29 @@ deploy_local() { info "Deploying to Cloud Run..." - # Update service.yaml with project ID and image - sed -i.bak "s/PROJECT_ID/$PROJECT_ID/g" service.yaml - sed -i.bak "s|gcr.io/PROJECT_ID/kernel-browser:latest|$image_name|g" service.yaml + # Choose appropriate service.yaml based on secrets availability + local service_file="service.yaml" + if [ "${USE_SECRETS:-false}" = "true" ]; then + if gcloud secrets describe twilio-account-sid --project="$PROJECT_ID" &>/dev/null && \ + gcloud secrets describe twilio-auth-token --project="$PROJECT_ID" &>/dev/null; then + service_file="service-secrets.yaml" + info "Using service-secrets.yaml with Secret Manager references" + else + warning "Secrets not found, falling back to standard service.yaml" + fi + fi + + # Update service file with project ID and image + cp "$service_file" "${service_file}.tmp" + sed -i.bak "s/PROJECT_ID/$PROJECT_ID/g" "${service_file}.tmp" + sed -i.bak "s|us-docker.pkg.dev/$PROJECT_ID/gcr.io/kernel-browser:latest|$image_name|g" "${service_file}.tmp" - gcloud run services replace service.yaml \ + gcloud run services replace "${service_file}.tmp" \ --region="$REGION" \ --project="$PROJECT_ID" - # Restore original service.yaml - mv service.yaml.bak service.yaml + # Clean up temporary files + rm -f "${service_file}.tmp" "${service_file}.tmp.bak" success "Local build and deployment completed" } @@ -255,9 +390,11 @@ main() { done check_prerequisites + load_env_file setup_project enable_apis create_service_account + setup_secrets update_submodules if [ "${LOCAL_BUILD:-false}" = "true" ]; then diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..42a2744 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + kernel-browser: + image: "kernel-browser:local" + container_name: "kernel-browser-local" + privileged: true + shm_size: 2gb + deploy: + resources: + limits: + memory: 8192M + ports: + # Chrome DevTools Protocol (matches kernel-images default) + - "9222:9222" + # Recording API (matches kernel-images default) + - "444:10001" + # WebRTC client interface + - "8080:8080" + # WebRTC UDP port range for local development + - "56000-56100:56000-56100/udp" + environment: + # Display settings + - DISPLAY_NUM=1 + - HEIGHT=768 + - WIDTH=1024 + # WebRTC settings + - ENABLE_WEBRTC=true + - NEKO_WEBRTC_EPR=56000-56100 + - NEKO_WEBRTC_NAT1TO1=127.0.0.1 + # Run as kernel user (not root) + - RUN_AS_ROOT=false + # Mount Chromium flags + - CHROMIUM_FLAGS=--user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --start-maximized --remote-allow-origins=* --no-sandbox --disable-setuid-sandbox + volumes: + # Persist recordings in local directory + - "./recordings:/recordings" + # Mount Chromium flags file (will be created by run script) + - "./kernel-images/images/chromium-headful/.tmp/chromium/flags:/chromium/flags:ro" + tmpfs: + - /dev/shm:size=2g + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 120s # Allow more time for startup + +volumes: + recordings: + driver: local \ No newline at end of file diff --git a/nginx-devtools-cloudrun.conf b/nginx-devtools-cloudrun.conf new file mode 100644 index 0000000..0514c8b --- /dev/null +++ b/nginx-devtools-cloudrun.conf @@ -0,0 +1,105 @@ +# nginx configuration for DevTools frontend in Cloud Run +worker_processes 1; +pid /tmp/nginx-devtools.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Temporary paths for non-root execution + client_body_temp_path /tmp/nginx_devtools_client_temp; + proxy_temp_path /tmp/nginx_devtools_proxy_temp; + fastcgi_temp_path /tmp/nginx_devtools_fastcgi_temp; + uwsgi_temp_path /tmp/nginx_devtools_uwsgi_temp; + scgi_temp_path /tmp/nginx_devtools_scgi_temp; + + # Logging + access_log /tmp/nginx-devtools-access.log; + error_log /tmp/nginx-devtools-error.log; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + server { + listen 8001; + server_name localhost; + + # Root directory for DevTools frontend + root /usr/share/nginx/devtools; + index inspector.html devtools_app.html index.html; + + # Compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json application/wasm; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # CORS headers for DevTools + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range" always; + + # Handle OPTIONS requests + if ($request_method = 'OPTIONS') { + return 204; + } + + # Cache control for static assets + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot|avif)$ { + expires 1d; + add_header Cache-Control "public, immutable"; + } + + # Specific handling for WebAssembly files + location ~ \.wasm$ { + add_header Content-Type application/wasm; + } + + # JSON files + location ~ \.json$ { + add_header Content-Type application/json; + } + + # Main location + location / { + try_files $uri $uri/ /index.html; + } + + # Specific paths for DevTools + location /front_end/ { + alias /usr/share/nginx/devtools/; + try_files $uri $uri/ =404; + } + + # Health check for DevTools service + location /health { + access_log off; + add_header Content-Type application/json; + return 200 '{"status": "healthy", "service": "devtools-frontend-cloudrun"}'; + } + + # Error pages + error_page 404 /404.html; + location = /404.html { + internal; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + internal; + } + } +} \ No newline at end of file diff --git a/nginx.conf b/nginx.conf index a0e20da..6d0137b 100644 --- a/nginx.conf +++ b/nginx.conf @@ -35,7 +35,7 @@ http { } server { - listen 8080; + listen 8888; server_name _; # Health check endpoint (required by Cloud Run) @@ -47,7 +47,7 @@ http { # WebRTC client (main interface) location / { - proxy_pass http://127.0.0.1:8080; + proxy_pass http://127.0.0.1:8081; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; @@ -103,5 +103,17 @@ http { proxy_read_timeout 86400; proxy_send_timeout 86400; } + + # Enhanced DevTools Frontend + location /devtools/ { + proxy_pass http://127.0.0.1:8001/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Accept-Encoding gzip; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + } } } \ No newline at end of file diff --git a/service-secrets.yaml b/service-secrets.yaml new file mode 100644 index 0000000..d265334 --- /dev/null +++ b/service-secrets.yaml @@ -0,0 +1,100 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kernel-browser + annotations: + run.googleapis.com/ingress: all + run.googleapis.com/ingress-status: all +spec: + template: + metadata: + annotations: + # Use second generation execution environment + run.googleapis.com/execution-environment: gen2 + # Disable CPU throttling for consistent performance + run.googleapis.com/cpu-throttling: "false" + # Increase startup timeout to 10 minutes for complex service startup + run.googleapis.com/timeout: "600" + # Auto-scaling settings + autoscaling.knative.dev/minScale: "1" + autoscaling.knative.dev/maxScale: "2" + spec: + # Allow multiple concurrent requests (browser can handle multiple tabs/requests) + containerConcurrency: 10 + # 1 hour timeout for long browser sessions + timeoutSeconds: 3600 + # Service account for GCP access + serviceAccountName: kernel-browser-sa + containers: + - name: kernel-browser + # This will be set during deployment + image: us-docker.pkg.dev/PROJECT_ID/gcr.io/kernel-browser:latest + ports: + - name: http1 + containerPort: 8080 + resources: + limits: + # 2 CPU cores (within quota limits) + cpu: "2" + # 4GiB memory (within quota limits) + memory: "4Gi" + requests: + cpu: "1" + memory: "2Gi" + env: + # Enable WebRTC for live viewing + - name: ENABLE_WEBRTC + value: "true" + # Run as non-root user (Cloud Run requirement) + - name: RUN_AS_ROOT + value: "false" + # Chrome optimizations for Cloud Run + - name: CHROMIUM_FLAGS + value: "--user-data-dir=/home/kernel/user-data --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --remote-allow-origins=* --no-sandbox --disable-setuid-sandbox --disable-features=VizDisplayCompositor" + # Display configuration + - name: DISPLAY_NUM + value: "1" + - name: HEIGHT + value: "768" + - name: WIDTH + value: "1024" + # Twilio API Key credentials from Secret Manager + - name: TWILIO_ACCOUNT_SID + valueFrom: + secretKeyRef: + name: twilio-account-sid + key: latest + - name: TWILIO_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: twilio-auth-token + key: latest + # Dynamic TURN credentials will be generated using above secrets + # The twilio-credential-updater.sh script will use these at startup + - name: NEKO_ICESERVERS + value: 'DYNAMIC' # Placeholder - will be replaced by twilio-credential-updater.sh + # TCP-only WebRTC configuration for Cloud Run + - name: NEKO_WEBRTC_TCPMUX + value: "0" # Disable TCP multiplexing (nginx handles port 8080) + - name: NEKO_WEBRTC_ICE_LITE + value: "true" # Use ICE-Lite mode for server + - name: NEKO_WEBRTC_ICE_SERVERS_ONLY_TURN + value: "true" # Only use TURN servers, no STUN + # Optional: Google Cloud Storage bucket for recordings + - name: GCS_BUCKET + value: "kernel-browser-recordings" + # API configuration + - name: KERNEL_IMAGES_API_PORT + value: "10001" + - name: KERNEL_IMAGES_API_FRAME_RATE + value: "10" + - name: KERNEL_IMAGES_API_MAX_SIZE_MB + value: "500" + - name: KERNEL_IMAGES_API_OUTPUT_DIR + value: "/tmp/recordings" + # Force new revision + - name: DEPLOYMENT_VERSION + value: "v13-tcp-only-webrtc" + traffic: + - percent: 100 + latestRevision: true \ No newline at end of file diff --git a/service.yaml b/service.yaml index 11d4a9d..c78c45b 100644 --- a/service.yaml +++ b/service.yaml @@ -13,11 +13,11 @@ spec: run.googleapis.com/execution-environment: gen2 # Disable CPU throttling for consistent performance run.googleapis.com/cpu-throttling: "false" - # Increase startup timeout - run.googleapis.com/timeout: "3600" + # Increase startup timeout to 10 minutes for complex service startup + run.googleapis.com/timeout: "600" # Auto-scaling settings autoscaling.knative.dev/minScale: "1" - autoscaling.knative.dev/maxScale: "10" + autoscaling.knative.dev/maxScale: "2" spec: # Allow multiple concurrent requests (browser can handle multiple tabs/requests) containerConcurrency: 10 @@ -28,19 +28,19 @@ spec: containers: - name: kernel-browser # This will be set during deployment - image: gcr.io/browseroperator/kernel-browser:latest + image: us-docker.pkg.dev/PROJECT_ID/gcr.io/kernel-browser:latest ports: - name: http1 containerPort: 8080 resources: limits: - # 4 CPU cores for smooth browser performance - cpu: "4" - # 8GiB memory (minimum required for Chromium + services) - memory: "8Gi" - requests: + # 2 CPU cores (within quota limits) cpu: "2" + # 4GiB memory (within quota limits) memory: "4Gi" + requests: + cpu: "1" + memory: "2Gi" env: # Enable WebRTC for live viewing - name: ENABLE_WEBRTC @@ -58,14 +58,18 @@ spec: value: "768" - name: WIDTH value: "1024" - # ICE servers configuration for WebRTC (includes both STUN and TURN) + # Twilio API Key credentials for dynamic TURN generation + # No Twilio credentials in fallback mode - credentials handled by twilio-credential-updater.sh + # TCP-only TURN servers for Cloud Run (no STUN/UDP) - name: NEKO_ICESERVERS - value: '[{"urls":["stun:global.stun.twilio.com:3478"]},{"urls":["turn:global.turn.twilio.com:3478?transport=udp"],"username":"464cefa09d5a8b4030b34b3faf15871b5efe0eef8331e9324f3f4f9144158ada","credential":"1Fm/UdpnNFbvfDPBtETUSZ4BhQsi0cubgLBdbScluPs="}]' - # WebRTC configuration - - name: NEKO_WEBRTC_TCPPORT - value: "8081" - - name: NEKO_WEBRTC_UDPPORT - value: "8082" + value: '[{"urls":["turn:openrelay.metered.ca:80?transport=tcp"],"username":"openrelayproject","credential":"openrelayproject"},{"urls":["turns:openrelay.metered.ca:443?transport=tcp"],"username":"openrelayproject","credential":"openrelayproject"}]' + # TCP-only WebRTC configuration for Cloud Run + - name: NEKO_WEBRTC_TCPMUX + value: "0" # Disable TCP multiplexing (nginx handles port 8080) + - name: NEKO_WEBRTC_ICE_LITE + value: "true" # Use ICE-Lite mode for server + - name: NEKO_WEBRTC_ICE_SERVERS_ONLY_TURN + value: "true" # Only use TURN servers, no STUN # Optional: Google Cloud Storage bucket for recordings - name: GCS_BUCKET value: "kernel-browser-recordings" @@ -78,6 +82,9 @@ spec: value: "500" - name: KERNEL_IMAGES_API_OUTPUT_DIR value: "/tmp/recordings" + # Force new revision + - name: DEPLOYMENT_VERSION + value: "v13-tcp-only-fallback" traffic: - percent: 100 latestRevision: true \ No newline at end of file diff --git a/start-chromium-cloudrun.sh b/start-chromium-cloudrun.sh deleted file mode 100644 index 8e00e59..0000000 --- a/start-chromium-cloudrun.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -set -o pipefail -o errexit -o nounset - -# Cloud Run optimized Chromium launcher - no runuser needed since we're already kernel user - -echo "Starting Chromium launcher (Cloud Run mode)" - -# Resolve internal port for the remote debugging interface -INTERNAL_PORT="${INTERNAL_PORT:-9223}" - -# Load additional Chromium flags from env and optional file -CHROMIUM_FLAGS="${CHROMIUM_FLAGS:-}" -if [[ -f /chromium/flags ]]; then - CHROMIUM_FLAGS="$CHROMIUM_FLAGS $(cat /chromium/flags)" -fi -echo "CHROMIUM_FLAGS: $CHROMIUM_FLAGS" - -# Always use display :1 and point DBus to the system bus socket -export DISPLAY=":1" -export DBUS_SESSION_BUS_ADDRESS="unix:path=/tmp/dbus/system_bus_socket" -export XDG_CONFIG_HOME=/home/kernel/.config -export XDG_CACHE_HOME=/home/kernel/.cache -export HOME=/home/kernel - -echo "Running chromium as kernel user (Cloud Run mode)" -exec chromium \ - --remote-debugging-port="$INTERNAL_PORT" \ - --user-data-dir=/home/kernel/user-data \ - --password-store=basic \ - --no-first-run \ - ${CHROMIUM_FLAGS:-} \ No newline at end of file diff --git a/supervisor/services-cloudrun/chromium.conf b/supervisor/services-cloudrun/chromium.conf new file mode 100644 index 0000000..d8413f5 --- /dev/null +++ b/supervisor/services-cloudrun/chromium.conf @@ -0,0 +1,10 @@ +[program:chromium] +command=/bin/bash -lc 'sleep 3 && DISPLAY=":1" DBUS_SESSION_BUS_ADDRESS="unix:path=/tmp/dbus/session_bus_socket" chromium --remote-debugging-port=9223 --remote-allow-origins=* --user-data-dir=/home/kernel/user-data --password-store=basic --no-first-run --disable-dev-shm-usage --disable-gpu --start-maximized --disable-software-rasterizer --no-sandbox --disable-setuid-sandbox --disable-features=VizDisplayCompositor --custom-devtools-frontend=http://localhost:8001/' +autostart=true +autorestart=true +startsecs=8 +priority=20 +stdout_logfile=/var/log/supervisord/chromium/chromium.log +stdout_logfile_maxbytes=50MB +redirect_stderr=true +environment=HOME="/home/kernel",USER="kernel",DISPLAY=":1",DBUS_SESSION_BUS_ADDRESS="unix:path=/tmp/dbus/session_bus_socket" \ No newline at end of file diff --git a/supervisor/services-cloudrun/dbus.conf b/supervisor/services-cloudrun/dbus.conf new file mode 100644 index 0000000..beb4cda --- /dev/null +++ b/supervisor/services-cloudrun/dbus.conf @@ -0,0 +1,10 @@ +[program:dbus] +command=/bin/bash -lc 'mkdir -p /tmp/dbus && dbus-uuidgen --ensure && dbus-daemon --session --address=unix:path=/tmp/dbus/session_bus_socket --nopidfile --nosyslog --nofork' +autostart=true +autorestart=true +startsecs=2 +priority=1 +stdout_logfile=/var/log/supervisord/dbus/dbus.log +stdout_logfile_maxbytes=50MB +redirect_stderr=true +environment=HOME="/home/kernel",USER="kernel",DBUS_SESSION_BUS_ADDRESS="unix:path=/tmp/dbus/session_bus_socket" \ No newline at end of file diff --git a/supervisor/services-cloudrun/devtools-frontend.conf b/supervisor/services-cloudrun/devtools-frontend.conf new file mode 100644 index 0000000..b0855ed --- /dev/null +++ b/supervisor/services-cloudrun/devtools-frontend.conf @@ -0,0 +1,11 @@ +[program:devtools-frontend] +command=/bin/bash -c 'cd /usr/share/nginx/devtools && python3 -m http.server 8001' +autostart=true +autorestart=true +startsecs=5 +priority=20 +stdout_logfile=/var/log/supervisord/devtools-frontend/devtools-frontend.log +stdout_logfile_maxbytes=50MB +redirect_stderr=true +environment=HOME="/home/kernel",USER="kernel" +user=kernel \ No newline at end of file diff --git a/supervisor/services-cloudrun/neko.conf b/supervisor/services-cloudrun/neko.conf new file mode 100644 index 0000000..969b62d --- /dev/null +++ b/supervisor/services-cloudrun/neko.conf @@ -0,0 +1,10 @@ +[program:neko] +command=/usr/bin/neko serve --server.static /var/www --server.bind 0.0.0.0:8081 +autostart=true +autorestart=true +startsecs=5 +priority=15 +stdout_logfile=/var/log/supervisord/neko/neko.log +stdout_logfile_maxbytes=50MB +redirect_stderr=true +environment=HOME="/home/kernel",USER="kernel",DISPLAY=":1",NEKO_WEBRTC_ICESERVERS_FRONTEND="",NEKO_WEBRTC_ICESERVERS_BACKEND="" \ No newline at end of file diff --git a/supervisor/services-cloudrun/xorg.conf b/supervisor/services-cloudrun/xorg.conf new file mode 100644 index 0000000..243c8f5 --- /dev/null +++ b/supervisor/services-cloudrun/xorg.conf @@ -0,0 +1,10 @@ +[program:xorg] +command=/usr/bin/Xorg :1 -config /etc/neko/xorg.conf -noreset -nolisten tcp +autostart=true +autorestart=true +startsecs=2 +priority=2 +stdout_logfile=/var/log/supervisord/xorg/xorg.log +stdout_logfile_maxbytes=50MB +redirect_stderr=true +environment=HOME="/home/kernel",USER="kernel" \ No newline at end of file diff --git a/twilio/README.md b/twilio/README.md new file mode 100644 index 0000000..3b77d32 --- /dev/null +++ b/twilio/README.md @@ -0,0 +1,79 @@ +# Twilio TURN Server Integration + +This folder contains scripts for integrating Twilio's Network Traversal Service to provide TURN server credentials for WebRTC in Cloud Run. + +## Scripts + +### `twilio-credential-updater.sh` +- **Purpose**: Called by `cloudrun-wrapper.sh` on container startup +- **Function**: Fetches fresh TURN credentials from Twilio API +- **Fallback**: Uses free TURN servers if Twilio fails +- **Environment Variables Required**: + - `TWILIO_ACCOUNT_SID` (API Key SID) + - `TWILIO_AUTH_TOKEN` (API Key Secret) + +### `twilio-token-service.js` +- **Purpose**: Node.js service for TURN credential generation +- **Features**: + - HTTP server mode (`--server` flag) + - One-time credential generation (default) + - Credential caching (1 hour) +- **Dependencies**: Express.js (for server mode) + +### `test-twilio-api.sh` +- **Purpose**: Test Twilio Network Traversal Service API +- **Usage**: `TWILIO_ACCOUNT_SID=xxx TWILIO_AUTH_TOKEN=xxx ./test-twilio-api.sh` +- **Output**: Formatted credentials for `NEKO_ICESERVERS` + +### `test-twilio-node.js` +- **Purpose**: Simple Node.js test for Twilio API +- **Usage**: Node.js version of the API test +- **Dependencies**: Only Node.js built-ins + +### `update-twilio-credentials.sh` +- **Purpose**: Update running Cloud Run service with fresh credentials +- **Usage**: Run periodically to refresh credentials +- **Features**: Direct Cloud Run service update + +## Integration + +The main integration point is in `../cloudrun-wrapper.sh`: + +```bash +# Get fresh Twilio TURN credentials if available +if [ -f /twilio-credential-updater.sh ]; then + echo "[cloudrun-wrapper] Getting fresh Twilio TURN credentials..." + source /twilio-credential-updater.sh +else + echo "[cloudrun-wrapper] Twilio updater not found, using credentials from environment" +fi +``` + +## Credentials Format + +Twilio Network Traversal Service returns credentials in this format: + +```json +{ + "ice_servers": [ + { + "url": "turn:global.turn.twilio.com:3478?transport=tcp", + "username": "long-generated-username", + "credential": "base64-encoded-credential" + } + ], + "ttl": "86400" +} +``` + +These are converted to neko format: + +```json +[ + { + "urls": ["turn:global.turn.twilio.com:3478?transport=tcp"], + "username": "long-generated-username", + "credential": "base64-encoded-credential" + } +] +``` \ No newline at end of file diff --git a/twilio/twilio-credential-updater.sh b/twilio/twilio-credential-updater.sh new file mode 100644 index 0000000..80255ea --- /dev/null +++ b/twilio/twilio-credential-updater.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# Twilio TURN Credential Updater for Cloud Run +# This script is called from cloudrun-wrapper.sh to get fresh credentials on startup + +set -e + +# Check if we're using dynamic credentials mode (from Secret Manager) +if [ "$NEKO_ICESERVERS" = "DYNAMIC" ]; then + echo "[twilio-updater] Dynamic credentials mode - will fetch fresh TURN credentials" +elif [ -n "$NEKO_ICESERVERS" ] && [ "$NEKO_ICESERVERS" != "DYNAMIC" ]; then + # NEKO_ICESERVERS is already set with actual credentials + echo "[twilio-updater] Using pre-configured TURN credentials" + return 0 2>/dev/null || exit 0 +fi + +# Twilio credentials (passed as environment variables) +ACCOUNT_SID="${TWILIO_ACCOUNT_SID}" +AUTH_TOKEN="${TWILIO_AUTH_TOKEN}" + +if [ -z "$ACCOUNT_SID" ] || [ -z "$AUTH_TOKEN" ]; then + echo "[twilio-updater] Warning: Twilio credentials not set, using TCP-only fallback TURN servers" + # Export TCP-only fallback servers (no STUN for Cloud Run) + export NEKO_ICESERVERS='[{"urls": ["turn:openrelay.metered.ca:80?transport=tcp"], "username": "openrelayproject", "credential": "openrelayproject"}, {"urls": ["turns:openrelay.metered.ca:443?transport=tcp"], "username": "openrelayproject", "credential": "openrelayproject"}]' + return 0 2>/dev/null || exit 0 +fi + +echo "[twilio-updater] Fetching fresh TURN credentials from Twilio..." + +# Get TURN credentials from Twilio API +response=$(curl -s -X POST \ + "https://api.twilio.com/2010-04-01/Accounts/${ACCOUNT_SID}/Tokens.json" \ + -u "${ACCOUNT_SID}:${AUTH_TOKEN}" 2>/dev/null) + +# Check if request was successful +if echo "$response" | grep -q "ice_servers"; then + # Format credentials for neko (TCP-only for Cloud Run) + ice_servers=$(echo "$response" | python3 -c " +import json +import sys +try: + data = json.load(sys.stdin) + servers = [] + for server in data.get('ice_servers', []): + if server.get('url', '').startswith('turn'): + url = server['url'] + # Force TCP transport for Cloud Run compatibility + if '?transport=' in url: + url = url.split('?transport=')[0] + url += '?transport=tcp' + servers.append({ + 'urls': [url], + 'username': server.get('username', ''), + 'credential': server.get('credential', '') + }) + + # Also add TLS version for redundancy + tls_url = url.replace('turn:', 'turns:').replace(':3478', ':5349') + servers.append({ + 'urls': [tls_url], + 'username': server.get('username', ''), + 'credential': server.get('credential', '') + }) + + # Remove STUN servers - only use TURN for Cloud Run + print(json.dumps(servers)) +except: + print('[]') +" 2>/dev/null) + + if [ -n "$ice_servers" ] && [ "$ice_servers" != "[]" ]; then + echo "[twilio-updater] Successfully retrieved TURN credentials" + export NEKO_ICESERVERS="$ice_servers" + else + echo "[twilio-updater] Failed to parse TURN credentials, using TCP-only fallback" + export NEKO_ICESERVERS='[{"urls": ["turn:openrelay.metered.ca:80?transport=tcp"], "username": "openrelayproject", "credential": "openrelayproject"}, {"urls": ["turns:openrelay.metered.ca:443?transport=tcp"], "username": "openrelayproject", "credential": "openrelayproject"}]' + fi +else + echo "[twilio-updater] Failed to get TURN credentials from Twilio, using TCP-only fallback" + echo "[twilio-updater] Response: ${response:0:100}..." + export NEKO_ICESERVERS='[{"urls": ["turn:openrelay.metered.ca:80?transport=tcp"], "username": "openrelayproject", "credential": "openrelayproject"}, {"urls": ["turns:openrelay.metered.ca:443?transport=tcp"], "username": "openrelayproject", "credential": "openrelayproject"}]' +fi + +echo "[twilio-updater] NEKO_ICESERVERS set to: ${NEKO_ICESERVERS:0:100}..." \ No newline at end of file diff --git a/twilio/update-twilio-credentials.sh b/twilio/update-twilio-credentials.sh new file mode 100755 index 0000000..3ce4364 --- /dev/null +++ b/twilio/update-twilio-credentials.sh @@ -0,0 +1,114 @@ +#!/bin/bash + +# Update Cloud Run service with fresh Twilio TURN credentials +# This script should be run periodically (e.g., every hour via cron) +# Run from the root directory: ./twilio/update-twilio-credentials.sh + +set -e + +# Load environment variables from .env file if it exists +if [ -f ../.env ]; then + set -a + . ../.env + set +a +elif [ -f .env ]; then + set -a + . .env + set +a +fi + +# Configuration +PROJECT_ID="${PROJECT_ID}" +SERVICE_NAME="kernel-browser" +REGION="${REGION:-us-central1}" + +# Twilio credentials (from environment or .env file) +TWILIO_ACCOUNT_SID="${TWILIO_ACCOUNT_SID}" +TWILIO_AUTH_TOKEN="${TWILIO_AUTH_TOKEN}" + +if [ -z "$PROJECT_ID" ]; then + echo "❌ Error: PROJECT_ID must be set" + echo " Set it in your .env file or export as environment variable:" + echo " export PROJECT_ID=your-project-id" + exit 1 +fi + +if [ -z "$TWILIO_ACCOUNT_SID" ] || [ -z "$TWILIO_AUTH_TOKEN" ]; then + echo "❌ Error: TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN must be set" + echo " Set them in your .env file or export as environment variables:" + echo " export TWILIO_ACCOUNT_SID=your_account_sid" + echo " export TWILIO_AUTH_TOKEN=your_auth_token" + exit 1 +fi + +echo "🔄 Fetching fresh TURN credentials from Twilio..." + +# Get TURN credentials from Twilio API +response=$(curl -s -X POST \ + "https://api.twilio.com/2010-04-01/Accounts/${TWILIO_ACCOUNT_SID}/Tokens.json" \ + -u "${TWILIO_ACCOUNT_SID}:${TWILIO_AUTH_TOKEN}") + +# Check if request was successful +if ! echo "$response" | grep -q "ice_servers"; then + echo "❌ Failed to get TURN credentials from Twilio" + echo "Response: $response" + exit 1 +fi + +# Format credentials for neko +ice_servers=$(echo "$response" | python3 -c " +import json +import sys +data = json.load(sys.stdin) +servers = [] +for server in data.get('ice_servers', []): + if server.get('url', '').startswith('turn'): + url = server['url'] + if 'transport=' not in url: + url += '?transport=tcp' + servers.append({ + 'urls': [url], + 'username': server.get('username', ''), + 'credential': server.get('credential', '') + }) +print(json.dumps(servers)) +") + +echo "✅ Received fresh TURN credentials" +echo " ICE Servers: $ice_servers" + +# Update Cloud Run service with new credentials +echo "🚀 Updating Cloud Run service..." + +# Create a temporary service.yaml with updated credentials +cat > /tmp/service-update.yaml <