11package main
22
33import (
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-
3224func 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) {
7467func 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
126115func (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
141138func (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