summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/config/config.go6
-rw-r--r--internal/handlers/proxy.go (renamed from internal/handlers/claudomator_proxy.go)49
-rw-r--r--internal/handlers/proxy_test.go (renamed from internal/handlers/claudomator_proxy_test.go)47
3 files changed, 55 insertions, 47 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index d3770f1..fe7bca5 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -42,6 +42,9 @@ type Config struct {
// Claudomator
ClaudomatorURL string // URL of Claudomator service
+
+ // Service gateway — additional upstream services proxied through doot's auth layer
+ PlaygroundURL string // URL of playground demo service (optional)
}
// Load reads configuration from environment variables
@@ -81,6 +84,9 @@ func Load() (*Config, error) {
// Claudomator
ClaudomatorURL: getEnvWithDefault("CLAUDOMATOR_URL", "http://127.0.0.1:8484"),
+
+ // Service gateway
+ PlaygroundURL: os.Getenv("PLAYGROUND_URL"),
}
// Validate required fields
diff --git a/internal/handlers/claudomator_proxy.go b/internal/handlers/proxy.go
index f3f0cd2..ce10a6a 100644
--- a/internal/handlers/claudomator_proxy.go
+++ b/internal/handlers/proxy.go
@@ -9,38 +9,34 @@ import (
"strings"
)
-// NewClaudomatorProxy returns an http.Handler that reverse-proxies requests to
-// targetURL, stripping the "/claudomator" prefix from the path. WebSocket
-// upgrade requests are handled via raw TCP hijacking to support long-lived
-// connections.
-func NewClaudomatorProxy(targetURL, allowedOrigin string) http.Handler {
+// NewServiceProxy returns an http.Handler that reverse-proxies requests to
+// targetURL, stripping pathPrefix from the request path before forwarding.
+// WebSocket upgrade requests are handled via raw TCP hijacking to support
+// long-lived connections.
+func NewServiceProxy(targetURL, allowedOrigin, pathPrefix string) http.Handler {
target, err := url.Parse(targetURL)
if err != nil {
- panic("claudomator: invalid target URL: " + err.Error())
+ panic("service proxy: invalid target URL: " + err.Error())
+ }
+
+ stripPrefix := func(p string) string {
+ s := strings.TrimPrefix(p, pathPrefix)
+ if s == "" {
+ s = "/"
+ }
+ return s
}
rp := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
-
- // Strip /claudomator prefix
- stripped := strings.TrimPrefix(req.URL.Path, "/claudomator")
- if stripped == "" {
- stripped = "/"
- }
- req.URL.Path = stripped
-
+ req.URL.Path = stripPrefix(req.URL.Path)
if req.URL.RawPath != "" {
- rawStripped := strings.TrimPrefix(req.URL.RawPath, "/claudomator")
- if rawStripped == "" {
- rawStripped = "/"
- }
- req.URL.RawPath = rawStripped
+ req.URL.RawPath = stripPrefix(req.URL.RawPath)
}
},
ModifyResponse: func(resp *http.Response) error {
- // Preserve Service-Worker-Allowed header
if swa := resp.Header.Get("Service-Worker-Allowed"); swa != "" {
resp.Header.Set("Service-Worker-Allowed", swa)
}
@@ -54,7 +50,7 @@ func NewClaudomatorProxy(targetURL, allowedOrigin string) http.Handler {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
- proxyWebSocket(w, r, target)
+ proxyWebSocket(w, r, target, pathPrefix)
return
}
rp.ServeHTTP(w, r)
@@ -62,8 +58,7 @@ func NewClaudomatorProxy(targetURL, allowedOrigin string) http.Handler {
}
// proxyWebSocket handles WebSocket upgrade via raw TCP hijacking.
-func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) {
- // Determine host:port for dialing
+func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL, pathPrefix string) {
host := target.Host
if target.Port() == "" {
switch target.Scheme {
@@ -81,16 +76,15 @@ func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) {
}
defer upstream.Close()
- // Rewrite path on the request before forwarding
r.URL.Scheme = target.Scheme
r.URL.Host = target.Host
- stripped := strings.TrimPrefix(r.URL.Path, "/claudomator")
+ stripped := strings.TrimPrefix(r.URL.Path, pathPrefix)
if stripped == "" {
stripped = "/"
}
r.URL.Path = stripped
if r.URL.RawPath != "" {
- rawStripped := strings.TrimPrefix(r.URL.RawPath, "/claudomator")
+ rawStripped := strings.TrimPrefix(r.URL.RawPath, pathPrefix)
if rawStripped == "" {
rawStripped = "/"
}
@@ -98,13 +92,11 @@ func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) {
}
r.RequestURI = r.URL.RequestURI()
- // Write the HTTP request to the upstream connection
if err := r.Write(upstream); err != nil {
http.Error(w, "bad gateway", http.StatusBadGateway)
return
}
- // Hijack the client connection
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "websocket not supported", http.StatusInternalServerError)
@@ -116,7 +108,6 @@ func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) {
}
defer clientConn.Close()
- // Bidirectional copy — no deadlines so long-lived WS connections survive
done := make(chan struct{}, 2)
go func() {
_, _ = io.Copy(upstream, clientConn)
diff --git a/internal/handlers/claudomator_proxy_test.go b/internal/handlers/proxy_test.go
index bb2842a..69d701d 100644
--- a/internal/handlers/claudomator_proxy_test.go
+++ b/internal/handlers/proxy_test.go
@@ -10,13 +10,13 @@ import (
"testing"
)
-func TestClaudomatorProxy_HTTP_Forward(t *testing.T) {
+func TestServiceProxy_HTTP_Forward(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello")
}))
defer backend.Close()
- proxy := NewClaudomatorProxy(backend.URL)
+ proxy := NewServiceProxy(backend.URL, "", "/claudomator")
req := httptest.NewRequest("GET", "/claudomator/", nil)
rr := httptest.NewRecorder()
@@ -30,7 +30,7 @@ func TestClaudomatorProxy_HTTP_Forward(t *testing.T) {
}
}
-func TestClaudomatorProxy_StripPrefix(t *testing.T) {
+func TestServiceProxy_StripPrefix(t *testing.T) {
var seenPath string
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenPath = r.URL.Path
@@ -38,7 +38,7 @@ func TestClaudomatorProxy_StripPrefix(t *testing.T) {
}))
defer backend.Close()
- proxy := NewClaudomatorProxy(backend.URL)
+ proxy := NewServiceProxy(backend.URL, "", "/claudomator")
req := httptest.NewRequest("GET", "/claudomator/api/tasks", nil)
rr := httptest.NewRecorder()
@@ -49,7 +49,7 @@ func TestClaudomatorProxy_StripPrefix(t *testing.T) {
}
}
-func TestClaudomatorProxy_RawPathStrip(t *testing.T) {
+func TestServiceProxy_RawPathStrip(t *testing.T) {
var seenRawPath string
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seenRawPath = r.URL.RawPath
@@ -57,26 +57,25 @@ func TestClaudomatorProxy_RawPathStrip(t *testing.T) {
}))
defer backend.Close()
- proxy := NewClaudomatorProxy(backend.URL)
+ proxy := NewServiceProxy(backend.URL, "", "/claudomator")
req := httptest.NewRequest("GET", "/claudomator/api/tasks%2Ffoo", nil)
rr := httptest.NewRecorder()
proxy.ServeHTTP(rr, req)
- // RawPath should have /claudomator stripped
if strings.HasPrefix(seenRawPath, "/claudomator") {
t.Errorf("upstream still sees /claudomator prefix in RawPath: %q", seenRawPath)
}
}
-func TestClaudomatorProxy_ServiceWorkerHeader(t *testing.T) {
+func TestServiceProxy_ServiceWorkerHeader(t *testing.T) {
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Service-Worker-Allowed", "/")
w.WriteHeader(http.StatusOK)
}))
defer backend.Close()
- proxy := NewClaudomatorProxy(backend.URL)
+ proxy := NewServiceProxy(backend.URL, "", "/claudomator")
req := httptest.NewRequest("GET", "/claudomator/sw.js", nil)
rr := httptest.NewRecorder()
@@ -87,6 +86,25 @@ func TestClaudomatorProxy_ServiceWorkerHeader(t *testing.T) {
}
}
+func TestServiceProxy_DifferentPrefix(t *testing.T) {
+ var seenPath string
+ backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ seenPath = r.URL.Path
+ w.WriteHeader(http.StatusOK)
+ }))
+ defer backend.Close()
+
+ proxy := NewServiceProxy(backend.URL, "", "/playground")
+
+ req := httptest.NewRequest("GET", "/playground/status", nil)
+ rr := httptest.NewRecorder()
+ proxy.ServeHTTP(rr, req)
+
+ if seenPath != "/status" {
+ t.Errorf("expected upstream to see /status, got %q", seenPath)
+ }
+}
+
// responseHijacker wraps httptest.ResponseRecorder and implements http.Hijacker
// so WebSocket hijack tests work without a real TCP connection.
type responseHijacker struct {
@@ -99,8 +117,7 @@ func (h *responseHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return h.serverConn, rw, nil
}
-func TestClaudomatorProxy_WebSocket(t *testing.T) {
- // Stand up a minimal WebSocket echo backend that speaks raw HTTP upgrade
+func TestServiceProxy_WebSocket(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
@@ -116,18 +133,15 @@ func TestClaudomatorProxy_WebSocket(t *testing.T) {
}
defer conn.Close()
- // Read the HTTP upgrade request from the proxy
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil || n == 0 {
return
}
- // Send a minimal HTTP 101 response so the proxy considers this a WS
resp := "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n"
_, _ = conn.Write([]byte(resp))
- // Echo one message: read "ping", write "pong"
msgBuf := make([]byte, 64)
n, _ = conn.Read(msgBuf)
if n > 0 {
@@ -137,9 +151,8 @@ func TestClaudomatorProxy_WebSocket(t *testing.T) {
targetURL := "http://" + listener.Addr().String()
- proxy := NewClaudomatorProxy(targetURL)
+ proxy := NewServiceProxy(targetURL, "", "/claudomator")
- // Build a client-side TCP pipe to simulate the browser side
clientSide, serverSide := net.Pipe()
defer clientSide.Close()
@@ -156,13 +169,11 @@ func TestClaudomatorProxy_WebSocket(t *testing.T) {
proxy.ServeHTTP(rh, req)
}()
- // Write a message from the "browser" side and read the echo
_, err = clientSide.Write([]byte("ping"))
if err != nil {
t.Fatal(err)
}
- // Read back — we expect "pong" from the echo server (or at least some data)
got := make([]byte, 64)
n, _ := clientSide.Read(got)
if n == 0 {