1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
package handlers
import (
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"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 {
target, err := url.Parse(targetURL)
if err != nil {
panic("claudomator: invalid target URL: " + err.Error())
}
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
if req.URL.RawPath != "" {
rawStripped := strings.TrimPrefix(req.URL.RawPath, "/claudomator")
if rawStripped == "" {
rawStripped = "/"
}
req.URL.RawPath = rawStripped
}
},
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)
}
return nil
},
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
if allowedOrigin != "" && r.Header.Get("Origin") != allowedOrigin {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
proxyWebSocket(w, r, target)
return
}
rp.ServeHTTP(w, r)
})
}
// proxyWebSocket handles WebSocket upgrade via raw TCP hijacking.
func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) {
// Determine host:port for dialing
host := target.Host
if target.Port() == "" {
switch target.Scheme {
case "https":
host += ":443"
default:
host += ":80"
}
}
upstream, err := net.Dial("tcp", host)
if err != nil {
http.Error(w, "bad gateway", http.StatusBadGateway)
return
}
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")
if stripped == "" {
stripped = "/"
}
r.URL.Path = stripped
if r.URL.RawPath != "" {
rawStripped := strings.TrimPrefix(r.URL.RawPath, "/claudomator")
if rawStripped == "" {
rawStripped = "/"
}
r.URL.RawPath = rawStripped
}
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)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
return
}
defer clientConn.Close()
// Bidirectional copy — no deadlines so long-lived WS connections survive
done := make(chan struct{}, 2)
go func() {
_, _ = io.Copy(upstream, clientConn)
done <- struct{}{}
}()
go func() {
_, _ = io.Copy(clientConn, upstream)
done <- struct{}{}
}()
<-done
}
|