Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@ node_modules
.env.local
.vercel
.env*.local
todo/
dist/

# Go compiled binaries
packages/http-proxy-server/http-proxy-server
packages/http-proxy-server/public/
8 changes: 8 additions & 0 deletions packages/http-proxy-server/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/vercel/sandbox/http-proxy-server

go 1.22.0

require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
)
4 changes: 4 additions & 0 deletions packages/http-proxy-server/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
144 changes: 144 additions & 0 deletions packages/http-proxy-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"

"github.com/vercel/sandbox/http-proxy-server/proxy"
"github.com/vercel/sandbox/http-proxy-server/ws"
)

const defaultConfigPath = "/tmp/vercel/http-proxy/config.json"

// ConnectionInfo is output to stdout as JSON for the TS client to parse.
type ConnectionInfo struct {
WsPort int `json:"wsPort"`
ProxyPort int `json:"proxyPort"`
Token string `json:"token"`
}

func main() {
var (
help = flag.Bool("help", false, "Show help")
wsPort = flag.Int("ws-port", 0, "Port for WebSocket server (0 for auto)")
proxyPort = flag.Int("proxy-port", 0, "Port for HTTP proxy on localhost (0 for auto)")
token = flag.String("token", "", "Authentication token (auto-generated if empty)")
debug = flag.Bool("debug", false, "Enable debug logging")
)

flag.Parse()

if *help {
printUsage()
return
}

logLevel := new(slog.LevelVar)
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: logLevel,
}))

if *debug {
logLevel.Set(slog.LevelDebug)
logger.Debug("Debug logging enabled")
}

// Start WebSocket server
wsServer, err := ws.NewServer(logger.With("component", "ws"), *token, *wsPort)
if err != nil {
logger.Error("Failed to create WebSocket server", "error", err)
os.Exit(1)
}

go func() {
logger.Info("WebSocket server starting", "port", wsServer.Port)
if err := wsServer.ListenAndServe(); err != nil {
logger.Error("WebSocket server error", "error", err)
os.Exit(1)
}
}()

// Generate CA for HTTPS MITM and install in system trust store
ca, err := proxy.NewCA()
if err != nil {
logger.Error("Failed to generate CA", "error", err)
os.Exit(1)
}

certPath := "/etc/pki/ca-trust/source/anchors/vc-proxy-ca.pem"
if err := os.MkdirAll(filepath.Dir(certPath), 0755); err != nil {
logger.Warn("Failed to create cert dir (may need sudo)", "error", err)
}
if err := os.WriteFile(certPath, ca.CertPEM(), 0644); err != nil {
logger.Warn("Failed to write CA cert (may need sudo)", "error", err)
} else {
// Update the system trust store
cmd := exec.Command("update-ca-trust", "extract")
if out, err := cmd.CombinedOutput(); err != nil {
logger.Warn("Failed to update CA trust", "error", err, "output", string(out))
} else {
logger.Info("Installed MITM CA certificate in system trust store")
}
}

// Start HTTP proxy server
proxyServer, err := proxy.NewServer(logger.With("component", "proxy"), wsServer.Hub, ca, *proxyPort)
if err != nil {
logger.Error("Failed to create proxy server", "error", err)
os.Exit(1)
}

// Output connection info as JSON to stdout (TS client reads this)
info := ConnectionInfo{
WsPort: wsServer.Port,
ProxyPort: proxyServer.Port,
Token: wsServer.Token,
}
infoJSON, _ := json.Marshal(info)
fmt.Println(string(infoJSON))

// Also persist to config file so subsequent clients can connect
if err := os.MkdirAll(filepath.Dir(defaultConfigPath), 0755); err != nil {
logger.Error("Failed to create config directory", "error", err)
} else if err := os.WriteFile(defaultConfigPath, infoJSON, 0644); err != nil {
logger.Error("Failed to write config file", "error", err)
}

// Wait for the TS client to send "ready" before accepting proxy requests
go func() {
<-wsServer.Hub.Ready()
logger.Info("TS client ready, proxy is now active")
}()

logger.Info("HTTP proxy server starting", "port", proxyServer.Port)
if err := proxyServer.ListenAndServe(); err != nil {
logger.Error("Proxy server error", "error", err)
os.Exit(1)
}
}

func printUsage() {
fmt.Printf(`HTTP Proxy Server - WebSocket Tunnel

USAGE:
http-proxy-server [OPTIONS]

OPTIONS:
--ws-port <port> Port for WebSocket server (0 for auto, default: 0)
--proxy-port <port> Port for HTTP proxy on localhost (0 for auto, default: 0)
--token <token> Authentication token (auto-generated if empty)
--debug Enable debug logging
--help Show this help message

ARCHITECTURE:
Programs inside the sandbox use HTTP_PROXY=http://<session>:x@localhost:<proxy-port>
to route requests through this proxy. The proxy forwards each request over WebSocket
to an external TypeScript client, which calls a per-session callback and returns the
response.
`)
}
13 changes: 13 additions & 0 deletions packages/http-proxy-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@vercel/http-proxy-server",
"version": "0.0.1",
"private": true,
"license": "Apache-2.0",
"scripts": {
"vercel-build": "./scripts/build-binaries.sh && ./scripts/build.sh",
"build": "turbo run build:install-script",
"test": "go test ./...",
"build:binaries": "scripts/build-binaries.sh",
"build:install-script": "scripts/build.sh"
}
}
96 changes: 96 additions & 0 deletions packages/http-proxy-server/protocol/messages.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package protocol

import "encoding/json"

// Message types sent between the Go proxy and the TypeScript client.

const (
TypeRequest = "request"
TypeResponse = "response"
TypeConnect = "connect"
TypeConnectResponse = "connect-response"
TypeReady = "ready"
TypeError = "error"
TypeRegister = "register"
TypeRegisterAck = "register-ack"
TypeUnregister = "unregister"
)

// Envelope is the top-level JSON frame on the WebSocket.
type Envelope struct {
Type string `json:"type"`
}

// ProxyRequest is sent from Go → TS when an HTTP request arrives.
type ProxyRequest struct {
Type string `json:"type"` // "request"
RequestID string `json:"requestId"`
SessionID string `json:"sessionId"`
Method string `json:"method"`
URL string `json:"url"`
Headers map[string][]string `json:"headers"`
Body string `json:"body,omitempty"` // base64
}

// ProxyResponse is sent from TS → Go with the callback result.
type ProxyResponse struct {
Type string `json:"type"` // "response"
RequestID string `json:"requestId"`
Status int `json:"status"`
Headers map[string][]string `json:"headers,omitempty"`
Body string `json:"body,omitempty"` // base64
}

// ConnectRequest is sent from Go → TS for HTTPS CONNECT tunneling.
type ConnectRequest struct {
Type string `json:"type"` // "connect"
RequestID string `json:"requestId"`
SessionID string `json:"sessionId"`
Host string `json:"host"`
}

// ConnectResponse is sent from TS → Go to allow/deny a CONNECT.
type ConnectResponse struct {
Type string `json:"type"` // "connect-response"
RequestID string `json:"requestId"`
Allow bool `json:"allow"`
}

// ReadyMessage is sent from TS → Go on initial handshake.
type ReadyMessage struct {
Type string `json:"type"` // "ready"
}

// ErrorMessage is sent from TS → Go when a callback fails.
type ErrorMessage struct {
Type string `json:"type"` // "error"
RequestID string `json:"requestId"`
Message string `json:"message"`
}

// RegisterMessage is sent from TS → Go to claim ownership of sessions.
type RegisterMessage struct {
Type string `json:"type"` // "register"
SessionIDs []string `json:"sessionIds"`
}

// RegisterAckMessage is sent from Go → TS to confirm registration.
type RegisterAckMessage struct {
Type string `json:"type"` // "register-ack"
SessionIDs []string `json:"sessionIds"`
}

// UnregisterMessage is sent from TS → Go to release session ownership.
type UnregisterMessage struct {
Type string `json:"type"` // "unregister"
SessionIDs []string `json:"sessionIds"`
}

// ParseType extracts the message type from a raw JSON frame.
func ParseType(data []byte) (string, error) {
var env Envelope
if err := json.Unmarshal(data, &env); err != nil {
return "", err
}
return env.Type, nil
}
Loading
Loading