Skip to content

Commit d6c6488

Browse files
1ncursioclaude
andcommitted
refactor: simplify http_routes.go to match codebase style
- Remove httpRouteMeta/httpRouteMetaKey context value indirection, read public host/scheme from X-Forwarded-* headers directly - Inline stripPrefix into rewriteRequest - Inline modifyResponse as closure in newReverseProxy - Simplify error messages and variable names Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c560e4f commit d6c6488

1 file changed

Lines changed: 92 additions & 125 deletions

File tree

cmd/portal-tunnel/http_routes.go

Lines changed: 92 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
import (
4-
"context"
54
"errors"
65
"fmt"
76
"net"
@@ -22,22 +21,15 @@ type httpRoute struct {
2221
proxy *httputil.ReverseProxy
2322
}
2423

25-
type httpRouteMeta struct {
26-
publicHost string
27-
publicScheme string
28-
}
29-
30-
type httpRouteMetaKey struct{}
31-
3224
func newHTTPRouteHandler(rawRoutes []string) (http.Handler, error) {
3325
if len(rawRoutes) == 0 {
3426
return nil, errors.New("at least one --http-route is required")
3527
}
3628

3729
routes := make([]*httpRoute, 0, len(rawRoutes))
3830
seen := make(map[string]struct{}, len(rawRoutes))
39-
for _, rawRoute := range rawRoutes {
40-
route, err := parseHTTPRoute(rawRoute)
31+
for _, raw := range rawRoutes {
32+
route, err := parseHTTPRoute(raw)
4133
if err != nil {
4234
return nil, err
4335
}
@@ -49,6 +41,7 @@ func newHTTPRouteHandler(rawRoutes []string) (http.Handler, error) {
4941
routes = append(routes, route)
5042
}
5143

44+
// longest-prefix-first
5245
sort.Slice(routes, func(i, j int) bool {
5346
if len(routes[i].prefix) == len(routes[j].prefix) {
5447
return routes[i].prefix < routes[j].prefix
@@ -57,12 +50,12 @@ func newHTTPRouteHandler(rawRoutes []string) (http.Handler, error) {
5750
})
5851

5952
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60-
requestPath := r.URL.Path
61-
if requestPath == "" {
62-
requestPath = "/"
53+
p := r.URL.Path
54+
if p == "" {
55+
p = "/"
6356
}
6457
for _, route := range routes {
65-
if route.prefix == "/" || requestPath == route.prefix || strings.HasPrefix(requestPath, route.prefix+"/") {
58+
if route.prefix == "/" || p == route.prefix || strings.HasPrefix(p, route.prefix+"/") {
6659
route.proxy.ServeHTTP(w, r)
6760
return
6861
}
@@ -74,62 +67,66 @@ func newHTTPRouteHandler(rawRoutes []string) (http.Handler, error) {
7467
func parseHTTPRoute(raw string) (*httpRoute, error) {
7568
raw = strings.TrimSpace(raw)
7669
if raw == "" {
77-
return nil, errors.New("invalid --http-route: expected PATH=UPSTREAM")
70+
return nil, errors.New("--http-route: expected PATH=UPSTREAM")
7871
}
7972

8073
prefixRaw, upstreamRaw, ok := strings.Cut(raw, "=")
8174
if !ok {
82-
return nil, fmt.Errorf("invalid --http-route %q: expected PATH=UPSTREAM", raw)
75+
return nil, fmt.Errorf("--http-route %q: expected PATH=UPSTREAM", raw)
8376
}
8477

8578
prefix := strings.TrimSpace(prefixRaw)
86-
switch {
87-
case prefix == "":
88-
return nil, fmt.Errorf("invalid --http-route prefix %q: %w", strings.TrimSpace(prefixRaw), errors.New("prefix is required"))
89-
case !strings.HasPrefix(prefix, "/"):
90-
return nil, fmt.Errorf("invalid --http-route prefix %q: %w", strings.TrimSpace(prefixRaw), errors.New("prefix must start with /"))
79+
if prefix == "" {
80+
return nil, fmt.Errorf("--http-route %q: prefix is required", raw)
81+
}
82+
if !strings.HasPrefix(prefix, "/") {
83+
return nil, fmt.Errorf("--http-route %q: prefix must start with /", raw)
9184
}
9285
prefix = utils.NormalizeURLPath(prefix)
9386

9487
upstreamInput := strings.TrimSpace(upstreamRaw)
9588
if upstreamInput == "" {
96-
return nil, fmt.Errorf("invalid --http-route upstream %q: %w", strings.TrimSpace(upstreamRaw), errors.New("upstream is required"))
89+
return nil, fmt.Errorf("--http-route %q: upstream is required", raw)
9790
}
98-
9991
if !strings.Contains(upstreamInput, "://") {
10092
target, err := utils.NormalizeLoopbackTarget(upstreamInput)
10193
if err != nil {
102-
return nil, fmt.Errorf("invalid --http-route upstream %q: %w", strings.TrimSpace(upstreamRaw), err)
94+
return nil, fmt.Errorf("--http-route %q: %w", raw, err)
10395
}
10496
upstreamInput = "http://" + target
10597
}
10698

10799
upstream, err := url.Parse(upstreamInput)
108100
if err != nil {
109-
return nil, fmt.Errorf("invalid --http-route upstream %q: %w", strings.TrimSpace(upstreamRaw), err)
101+
return nil, fmt.Errorf("--http-route %q: %w", raw, err)
110102
}
111103
if upstream.Host == "" {
112-
return nil, fmt.Errorf("invalid --http-route upstream %q: %w", strings.TrimSpace(upstreamRaw), errors.New("upstream host is required"))
104+
return nil, fmt.Errorf("--http-route %q: upstream host is required", raw)
113105
}
114106
if upstream.Scheme != "http" && upstream.Scheme != "https" {
115-
return nil, fmt.Errorf("invalid --http-route upstream %q: %w", strings.TrimSpace(upstreamRaw), errors.New("upstream scheme must be http or https"))
107+
return nil, fmt.Errorf("--http-route %q: scheme must be http or https", raw)
116108
}
117109
upstream.Fragment = ""
118110
upstream.Path = utils.NormalizeURLPath(upstream.Path)
119111

120-
return &httpRoute{
121-
prefix: prefix,
122-
upstream: upstream,
123-
}, nil
112+
return &httpRoute{prefix: prefix, upstream: upstream}, nil
124113
}
125114

126115
func (r *httpRoute) newReverseProxy() *httputil.ReverseProxy {
127116
return &httputil.ReverseProxy{
128-
Rewrite: r.rewriteRequest,
129-
ModifyResponse: r.modifyResponse,
117+
Rewrite: r.rewriteRequest,
118+
ModifyResponse: func(resp *http.Response) error {
119+
if resp == nil || resp.Request == nil {
120+
return nil
121+
}
122+
publicHost := resp.Request.Header.Get("X-Forwarded-Host")
123+
publicScheme := resp.Request.Header.Get("X-Forwarded-Proto")
124+
r.rewriteLocation(resp.Header, publicHost, publicScheme)
125+
r.rewriteSetCookies(resp.Header, publicHost)
126+
return nil
127+
},
130128
ErrorHandler: func(w http.ResponseWriter, req *http.Request, err error) {
131-
log.Error().
132-
Err(err).
129+
log.Error().Err(err).
133130
Str("route_prefix", r.prefix).
134131
Str("upstream", r.upstream.String()).
135132
Msg("http route proxy failed")
@@ -139,101 +136,74 @@ func (r *httpRoute) newReverseProxy() *httputil.ReverseProxy {
139136
}
140137

141138
func (r *httpRoute) rewriteRequest(pr *httputil.ProxyRequest) {
142-
outboundPath, outboundRawPath := r.stripPrefix(pr.In.URL.Path, pr.In.URL.RawPath)
143-
pr.Out.URL.Path = outboundPath
144-
pr.Out.URL.RawPath = outboundRawPath
139+
// strip route prefix from the path before forwarding
140+
reqPath := utils.NormalizeURLPath(pr.In.URL.Path)
141+
rawPath := pr.In.URL.RawPath
142+
if r.prefix != "/" {
143+
if reqPath == r.prefix {
144+
reqPath = "/"
145+
rawPath = ""
146+
} else {
147+
reqPath = strings.TrimPrefix(reqPath, r.prefix)
148+
if reqPath == "" {
149+
reqPath = "/"
150+
}
151+
if rawPath == r.prefix {
152+
rawPath = "/"
153+
} else if strings.HasPrefix(rawPath, r.prefix+"/") {
154+
rawPath = strings.TrimPrefix(rawPath, r.prefix)
155+
}
156+
}
157+
}
158+
159+
pr.Out.URL.Path = reqPath
160+
pr.Out.URL.RawPath = rawPath
145161
pr.Out.URL.RawQuery = pr.In.URL.RawQuery
146162
pr.SetURL(r.upstream)
147163
pr.SetXForwarded()
148-
if r.prefix != "/" {
149-
pr.Out.Header.Set("X-Forwarded-Prefix", r.prefix)
150-
}
151164

152-
publicScheme := "http"
153-
if pr.In.TLS != nil {
154-
publicScheme = "https"
155-
} else {
156-
proto := strings.TrimSpace(pr.In.Header.Get("X-Forwarded-Proto"))
157-
if first, _, ok := strings.Cut(proto, ","); ok {
158-
proto = first
159-
}
165+
// SetXForwarded checks pr.In.TLS, but behind a TLS-terminating proxy
166+
// the inbound X-Forwarded-Proto carries the real client scheme.
167+
if pr.In.TLS == nil {
168+
proto, _, _ := strings.Cut(pr.In.Header.Get("X-Forwarded-Proto"), ",")
160169
if proto = strings.ToLower(strings.TrimSpace(proto)); proto != "" {
161-
publicScheme = proto
170+
pr.Out.Header.Set("X-Forwarded-Proto", proto)
162171
}
163172
}
164173

165-
meta := httpRouteMeta{
166-
publicHost: pr.In.Host,
167-
publicScheme: publicScheme,
168-
}
169-
pr.Out = pr.Out.WithContext(context.WithValue(pr.Out.Context(), httpRouteMetaKey{}, meta))
170-
}
171-
172-
func (r *httpRoute) modifyResponse(resp *http.Response) error {
173-
if resp == nil || resp.Request == nil {
174-
return nil
175-
}
176-
177-
meta, _ := resp.Request.Context().Value(httpRouteMetaKey{}).(httpRouteMeta)
178-
r.rewriteLocation(resp.Header, meta)
179-
r.rewriteSetCookies(resp.Header, meta.publicHost)
180-
return nil
181-
}
182-
183-
func (r *httpRoute) stripPrefix(requestPath, rawPath string) (string, string) {
184-
requestPath = utils.NormalizeURLPath(requestPath)
185-
if r.prefix == "/" {
186-
return requestPath, rawPath
187-
}
188-
if requestPath == r.prefix {
189-
return "/", ""
190-
}
191-
192-
trimmedPath := strings.TrimPrefix(requestPath, r.prefix)
193-
if trimmedPath == "" {
194-
trimmedPath = "/"
195-
}
196-
197-
trimmedRawPath := rawPath
198-
if trimmedRawPath != "" {
199-
if trimmedRawPath == r.prefix {
200-
trimmedRawPath = "/"
201-
} else if strings.HasPrefix(trimmedRawPath, r.prefix+"/") {
202-
trimmedRawPath = strings.TrimPrefix(trimmedRawPath, r.prefix)
203-
}
174+
if r.prefix != "/" {
175+
pr.Out.Header.Set("X-Forwarded-Prefix", r.prefix)
204176
}
205-
return trimmedPath, trimmedRawPath
206177
}
207178

208-
func (r *httpRoute) rewriteLocation(header http.Header, meta httpRouteMeta) {
209-
location := strings.TrimSpace(header.Get("Location"))
179+
func (r *httpRoute) rewriteLocation(header http.Header, publicHost, publicScheme string) {
180+
location := header.Get("Location")
210181
if location == "" {
211182
return
212183
}
213-
214184
parsed, err := url.Parse(location)
215185
if err != nil {
216186
return
217187
}
218188

219-
var mappedPath string
220189
switch {
221190
case parsed.IsAbs():
222191
if !strings.EqualFold(parsed.Scheme, r.upstream.Scheme) || !strings.EqualFold(parsed.Host, r.upstream.Host) {
223192
return
224193
}
225-
parsed.Scheme = meta.publicScheme
226-
parsed.Host = meta.publicHost
227-
case strings.HasPrefix(location, "/") && (len(location) == 1 || (location[1] != '/' && location[1] != '\\')):
194+
parsed.Scheme = publicScheme
195+
parsed.Host = publicHost
196+
case strings.HasPrefix(location, "/") && parsed.Host == "" && (len(location) == 1 || location[1] != '\\'):
197+
// server-relative redirect
228198
default:
229199
return
230200
}
231201

232-
mappedPath = r.mapUpstreamPathToPublic(parsed.Path)
233-
if !strings.HasPrefix(mappedPath, "/") || (len(mappedPath) > 1 && (mappedPath[1] == '/' || mappedPath[1] == '\\')) {
202+
mapped := r.mapUpstreamPathToPublic(parsed.Path)
203+
if !strings.HasPrefix(mapped, "/") || (len(mapped) > 1 && (mapped[1] == '/' || mapped[1] == '\\')) {
234204
return
235205
}
236-
parsed.Path = mappedPath
206+
parsed.Path = mapped
237207
parsed.RawPath = ""
238208
header.Set("Location", parsed.String())
239209
}
@@ -244,12 +214,13 @@ func (r *httpRoute) rewriteSetCookies(header http.Header, publicHost string) {
244214
return
245215
}
246216

247-
publicDomain := strings.ToLower(strings.TrimSpace(publicHost))
217+
publicDomain := publicHost
248218
if host, port, err := net.SplitHostPort(publicDomain); err == nil && port != "" {
249219
publicDomain = host
250220
}
251-
publicDomain = strings.Trim(publicDomain, "[]")
252-
upstreamDomain := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(r.upstream.Hostname()), "."))
221+
publicDomain = strings.ToLower(strings.Trim(publicDomain, "[]"))
222+
upstreamDomain := strings.ToLower(r.upstream.Hostname())
223+
253224
header.Del("Set-Cookie")
254225
for _, value := range values {
255226
cookie, err := http.ParseSetCookie(value)
@@ -259,26 +230,25 @@ func (r *httpRoute) rewriteSetCookies(header http.Header, publicHost string) {
259230
}
260231

261232
changed := false
262-
if strings.TrimSpace(cookie.Path) != "" {
263-
rewrittenPath := r.mapUpstreamPathToPublic(cookie.Path)
264-
if rewrittenPath != cookie.Path {
265-
cookie.Path = rewrittenPath
233+
if cookie.Path != "" {
234+
if rewritten := r.mapUpstreamPathToPublic(cookie.Path); rewritten != cookie.Path {
235+
cookie.Path = rewritten
266236
changed = true
267237
}
268238
}
269239

270-
currentDomain := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(cookie.Domain), "."))
271-
if currentDomain != "" && currentDomain != publicDomain &&
272-
(currentDomain == upstreamDomain || utils.IsLocalRelayHost(currentDomain)) {
240+
domain := strings.ToLower(strings.TrimPrefix(cookie.Domain, "."))
241+
if domain != "" && domain != publicDomain &&
242+
(domain == upstreamDomain || utils.IsLocalRelayHost(domain)) {
273243
cookie.Domain = ""
274244
changed = true
275245
}
276246

277247
if changed {
278248
header.Add("Set-Cookie", cookie.String())
279-
continue
249+
} else {
250+
header.Add("Set-Cookie", value)
280251
}
281-
header.Add("Set-Cookie", value)
282252
}
283253
}
284254

@@ -289,23 +259,20 @@ func (r *httpRoute) mapUpstreamPathToPublic(raw string) string {
289259
}
290260

291261
base := utils.NormalizeURLPath(r.upstream.Path)
292-
publicRest := raw
293-
switch {
294-
case base == "/":
295-
case raw == base:
296-
publicRest = "/"
297-
case strings.HasPrefix(raw, base+"/"):
298-
publicRest = strings.TrimPrefix(raw, base)
262+
rest := raw
263+
if base != "/" {
264+
if raw == base {
265+
rest = "/"
266+
} else if strings.HasPrefix(raw, base+"/") {
267+
rest = strings.TrimPrefix(raw, base)
268+
}
299269
}
300270

301271
if r.prefix == "/" {
302-
return publicRest
272+
return rest
303273
}
304-
if publicRest == "/" {
274+
if rest == "/" {
305275
return r.prefix
306276
}
307-
if strings.HasPrefix(publicRest, "/") {
308-
return r.prefix + publicRest
309-
}
310-
return r.prefix + "/" + publicRest
277+
return r.prefix + rest
311278
}

0 commit comments

Comments
 (0)