sketch: proxy to ports via p<port>.localhost Host headers
Add support for proxying requests based on Host header patterns.
When Host header matches p<port>.localhost, proxy the request to localhost:<port>.
- ParsePortProxyHost() extracts port from p8000.localhost format
- proxyToPort() handles generic port proxying with validation
- Supports any valid port (1-65535) via p<port>.localhost pattern
- Comprehensive tests for parsing and validation
Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: saa324eab0e9b3addk
diff --git a/loop/server/loophttp.go b/loop/server/loophttp.go
index bef4982..7f31401 100644
--- a/loop/server/loophttp.go
+++ b/loop/server/loophttp.go
@@ -13,7 +13,9 @@
"io/fs"
"log/slog"
"net/http"
+ "net/http/httputil"
"net/http/pprof"
+ "net/url"
"os"
"os/exec"
"path/filepath"
@@ -131,9 +133,68 @@
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ // Check if Host header matches "p<port>.localhost" pattern and proxy to that port
+ if port := s.ParsePortProxyHost(r.Host); port != "" {
+ s.proxyToPort(w, r, port)
+ return
+ }
+
s.mux.ServeHTTP(w, r)
}
+// ParsePortProxyHost checks if host matches "p<port>.localhost" pattern and returns the port
+func (s *Server) ParsePortProxyHost(host string) string {
+ // Remove port suffix if present (e.g., "p8000.localhost:8080" -> "p8000.localhost")
+ hostname := host
+ if idx := strings.LastIndex(host, ":"); idx > 0 {
+ hostname = host[:idx]
+ }
+
+ // Check if hostname matches p<port>.localhost pattern
+ if strings.HasSuffix(hostname, ".localhost") {
+ prefix := strings.TrimSuffix(hostname, ".localhost")
+ if strings.HasPrefix(prefix, "p") && len(prefix) > 1 {
+ port := prefix[1:] // Remove 'p' prefix
+ // Basic validation - port should be numeric and in valid range
+ if portNum, err := strconv.Atoi(port); err == nil && portNum > 0 && portNum <= 65535 {
+ return port
+ }
+ }
+ }
+
+ return ""
+}
+
+// proxyToPort proxies the request to localhost:<port>
+func (s *Server) proxyToPort(w http.ResponseWriter, r *http.Request, port string) {
+ // Create a reverse proxy to localhost:<port>
+ target, err := url.Parse(fmt.Sprintf("http://localhost:%s", port))
+ if err != nil {
+ http.Error(w, "Failed to parse proxy target", http.StatusInternalServerError)
+ return
+ }
+
+ proxy := httputil.NewSingleHostReverseProxy(target)
+
+ // Customize the Director to modify the request
+ originalDirector := proxy.Director
+ proxy.Director = func(req *http.Request) {
+ originalDirector(req)
+ // Set the target host
+ req.URL.Host = target.Host
+ req.URL.Scheme = target.Scheme
+ req.Host = target.Host
+ }
+
+ // Handle proxy errors
+ proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
+ slog.Error("Proxy error", "error", err, "target", target.String(), "port", port)
+ http.Error(w, "Proxy error: "+err.Error(), http.StatusBadGateway)
+ }
+
+ proxy.ServeHTTP(w, r)
+}
+
// New creates a new HTTP server.
func New(agent loop.CodingAgent, logFile *os.File) (*Server, error) {
s := &Server{
diff --git a/loop/server/loophttp_test.go b/loop/server/loophttp_test.go
index 0903774..adda4e3 100644
--- a/loop/server/loophttp_test.go
+++ b/loop/server/loophttp_test.go
@@ -581,3 +581,82 @@
t.Errorf("Expected status code %d, got %d", http.StatusMethodNotAllowed, status)
}
}
+
+func TestParsePortProxyHost(t *testing.T) {
+ tests := []struct {
+ name string
+ host string
+ wantPort string
+ }{
+ {
+ name: "valid port proxy host",
+ host: "p8000.localhost",
+ wantPort: "8000",
+ },
+ {
+ name: "valid port proxy host with port suffix",
+ host: "p8000.localhost:8080",
+ wantPort: "8000",
+ },
+ {
+ name: "different port",
+ host: "p3000.localhost",
+ wantPort: "3000",
+ },
+ {
+ name: "regular localhost",
+ host: "localhost",
+ wantPort: "",
+ },
+ {
+ name: "different domain",
+ host: "p8000.example.com",
+ wantPort: "",
+ },
+ {
+ name: "missing p prefix",
+ host: "8000.localhost",
+ wantPort: "",
+ },
+ {
+ name: "invalid port",
+ host: "pabc.localhost",
+ wantPort: "",
+ },
+ {
+ name: "just p prefix",
+ host: "p.localhost",
+ wantPort: "",
+ },
+ {
+ name: "port too high",
+ host: "p99999.localhost",
+ wantPort: "",
+ },
+ {
+ name: "port zero",
+ host: "p0.localhost",
+ wantPort: "",
+ },
+ {
+ name: "negative port",
+ host: "p-1.localhost",
+ wantPort: "",
+ },
+ }
+
+ // Create a test server to access the method
+ s, err := server.New(nil, nil)
+ if err != nil {
+ t.Fatalf("Failed to create server: %v", err)
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotPort := s.ParsePortProxyHost(tt.host)
+ if gotPort != tt.wantPort {
+ t.Errorf("parsePortProxyHost(%q) = %q, want %q", tt.host, gotPort, tt.wantPort)
+ }
+ })
+ }
+}