Skip to content

Commit 1a97a9e

Browse files
committed
improve proxy
1 parent 1871ddd commit 1a97a9e

5 files changed

Lines changed: 129 additions & 52 deletions

File tree

src/cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ func init() {
9292
if err != nil {
9393
panic(fmt.Errorf("fatal binding flag: %w", err))
9494
}
95+
96+
// Other config defaults
97+
viper.SetDefault("proxy.maxIdleConns", 1000)
98+
viper.SetDefault("proxy.maxIdleConnsPerHost", 100)
99+
viper.SetDefault("proxy.timeout", "90s")
100+
viper.SetDefault("proxy.compression", true)
95101
}
96102

97103
func initConfig() {

src/config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
server:
22
port: 8080
33

4+
proxy:
5+
maxIdleConns: 1000
6+
maxIdleConnsPerHost: 500
7+
timeout: 30s
8+
compression: false
9+
410
output:
511
debug: true
612
format: long

src/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
require (
66
github.com/fsnotify/fsnotify v1.8.0
77
github.com/gin-gonic/gin v1.10.0
8+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
89
github.com/sierrasoftworks/humane-errors-go v0.0.0-20241125132722-d032d7dd359e
910
github.com/spf13/cobra v1.9.1
1011
github.com/spf13/viper v1.20.1

src/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
3737
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
3838
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
3939
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
40+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
41+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
4042
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
4143
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
4244
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

src/pkg/proxy/proxy.go

Lines changed: 114 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
package proxy
22

33
import (
4+
"context"
45
"fmt"
56
"github.com/SpechtLabs/StaticPages/pkg/config"
7+
"github.com/golang/groupcache/singleflight"
68
"github.com/sierrasoftworks/humane-errors-go"
9+
"github.com/spf13/viper"
710
"github.com/uptrace/opentelemetry-go-extra/otelzap"
811
"go.uber.org/zap"
912
"net/http"
1013
"net/http/httputil"
1114
"net/url"
1215
"path"
1316
"strings"
17+
"sync"
18+
"time"
1419
)
1520

1621
type Proxy struct {
1722
zapLog *otelzap.Logger
1823
pages map[string]*config.Page
1924
proxy *httputil.ReverseProxy
25+
group singleflight.Group
2026
}
2127

2228
func NewProxy(zapLog *otelzap.Logger, pages []*config.Page) *Proxy {
@@ -37,62 +43,55 @@ func NewProxy(zapLog *otelzap.Logger, pages []*config.Page) *Proxy {
3743
}
3844

3945
p.proxy = &httputil.ReverseProxy{
40-
// Add a proxy director
41-
Director: p.Director,
42-
43-
// Add error handler to log errors
44-
ErrorHandler: p.ErrorHandler,
45-
46-
// Add response modifier to log response status
47-
ModifyResponse: p.ModifyResponse,
46+
Director: p.Director, // Add a proxy director
47+
ErrorHandler: p.ErrorHandler, // Add error handler to log errors
48+
ModifyResponse: p.ModifyResponse, // Add response modifier to log response status
49+
50+
// Allow transport configuration provided by user
51+
Transport: &http.Transport{
52+
MaxIdleConns: viper.GetInt("proxy.maxIdleConns"),
53+
MaxIdleConnsPerHost: viper.GetInt("proxy.maxIdleConnsPerHost"),
54+
IdleConnTimeout: viper.GetDuration("proxy.timeout"),
55+
DisableCompression: !viper.GetBool("proxy.compression"),
56+
},
4857
}
4958

5059
return p
5160
}
5261

5362
func (p *Proxy) Director(req *http.Request) {
54-
host := req.Host
63+
if req.Context().Err() != nil {
64+
p.zapLog.Ctx(req.Context()).Warn("request context canceled", zap.String("url", req.URL.String()), zap.String("path", req.URL.Path))
65+
return
66+
}
5567

56-
if strings.Contains(host, ":") {
57-
host = strings.Split(host, ":")[0]
68+
originalPath := req.URL.Path
69+
requestUrl := req.Host
70+
if strings.Contains(requestUrl, ":") {
71+
requestUrl = strings.Split(requestUrl, ":")[0]
5872
}
5973

60-
page, ok := p.pages[host]
74+
page, ok := p.pages[requestUrl]
6175
if !ok {
62-
p.zapLog.Error("no page found for host", zap.String("host", host))
76+
p.zapLog.Ctx(req.Context()).Error("no page found for requestUrl", zap.String("requestUrl", requestUrl))
6377
return
6478
}
6579

66-
targetURL, err := url.Parse(page.Proxy.URL.String())
80+
backendUrl, err := url.Parse(page.Proxy.URL.String())
6781
if err != nil {
68-
p.zapLog.Error("invalid target URL", zap.Error(err), zap.String("url", page.Proxy.URL.String()))
82+
p.zapLog.Ctx(req.Context()).Error("invalid target URL", zap.Error(err), zap.String("url", page.Proxy.URL.String()))
6983
return
7084
}
7185

72-
originalPath := req.URL.Path
73-
74-
// Create a clean path without double slashes
75-
targetPath := path.Clean(fmt.Sprintf("/%s/%s",
76-
page.Proxy.Path,
77-
originalPath,
78-
))
79-
80-
searchPath := append([]string{""}, page.Proxy.SearchPath...)
81-
82-
for _, lookupPath := range searchPath {
83-
testTarget := path.Clean(fmt.Sprintf("/%s/%s",
84-
targetPath,
85-
lookupPath,
86-
))
87-
88-
if resp, err := http.Head(targetURL.String() + testTarget); err == nil && resp.StatusCode < http.StatusBadRequest {
89-
targetPath = testTarget
90-
break
91-
}
86+
// Find the actual html document we are looking for
87+
targetPath, err := p.lookupPath(req.Context(), page, requestUrl, backendUrl, path.Clean(fmt.Sprintf("/%s/%s", page.Proxy.Path, originalPath)))
88+
if err != nil {
89+
p.zapLog.Ctx(req.Context()).Error("no valid path found", zap.String("original_path", originalPath), zap.String("target_path", targetPath))
90+
return
9291
}
9392

94-
req.URL.Scheme = targetURL.Scheme
95-
req.URL.Host = targetURL.Host
93+
req.URL.Scheme = backendUrl.Scheme
94+
req.URL.Host = backendUrl.Host
9695
req.URL.Path = targetPath
9796

9897
// Clear the RequestURI as it's required for client requests
@@ -104,42 +103,50 @@ func (p *Proxy) Director(req *http.Request) {
104103
}
105104

106105
req.Header.Set("X-Forwarded-Host", req.Host)
107-
req.Header.Set("X-Origin-Host", targetURL.Host)
106+
req.Header.Set("X-Origin-Host", backendUrl.Host)
108107

109108
// Log the request transformation
110-
p.zapLog.Debug("transforming request",
109+
p.zapLog.Ctx(req.Context()).Debug("transforming request",
111110
zap.String("original_path", originalPath),
112111
zap.String("target_path", targetPath),
113-
zap.String("target_server", targetURL.String()),
112+
zap.String("target_server", backendUrl.String()),
114113
zap.String("target_url", req.URL.String()),
115114
)
116115
}
117116

118117
func (p *Proxy) ErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
119118
responseCode := http.StatusBadGateway
120119

121-
p.zapLog.Error("proxy error",
122-
zap.String("error", err.Error()),
123-
zap.String("url", r.URL.String()),
124-
)
125-
126120
switch err.Error() {
127121
case "context canceled":
128122
responseCode = 499 // Nginx' non-standard code for when a client closes the connection
129123
}
130124

125+
p.zapLog.Ctx(r.Context()).Error("proxy error",
126+
zap.String("error", err.Error()),
127+
zap.String("url", r.URL.String()),
128+
zap.Int("status", responseCode),
129+
)
130+
131131
http.Error(w, err.Error(), responseCode)
132132
}
133133

134134
func (p *Proxy) ModifyResponse(r *http.Response) error {
135135
if r.StatusCode >= 300 {
136-
dump, _ := httputil.DumpResponse(r, true)
137-
138-
p.zapLog.Debug("received response",
139-
zap.Int("status", r.StatusCode),
140-
zap.String("url", r.Request.URL.String()),
141-
zap.ByteString("url", dump),
142-
)
136+
if p.zapLog.Core().Enabled(zap.DebugLevel) {
137+
dump, _ := httputil.DumpResponse(r, true)
138+
139+
p.zapLog.Ctx(r.Request.Context()).Debug("received response",
140+
zap.Int("status", r.StatusCode),
141+
zap.String("url", r.Request.URL.String()),
142+
zap.ByteString("url", dump),
143+
)
144+
} else {
145+
p.zapLog.Ctx(r.Request.Context()).Info("received response",
146+
zap.Int("status", r.StatusCode),
147+
zap.String("url", r.Request.URL.String()),
148+
)
149+
}
143150
}
144151

145152
return nil
@@ -176,3 +183,58 @@ func (p *Proxy) Serve(addr string) humane.Error {
176183

177184
return nil
178185
}
186+
187+
func (p *Proxy) probePath(ctx context.Context, url *url.URL, location string) (int, error) {
188+
// create a http client with short timeout for fast failure
189+
client := &http.Client{
190+
Timeout: 2 * time.Second,
191+
}
192+
193+
req, _ := http.NewRequestWithContext(ctx, http.MethodHead, url.String()+location, nil)
194+
resp, err := client.Do(req)
195+
if err != nil {
196+
return http.StatusNotFound, err
197+
}
198+
199+
return resp.StatusCode, err
200+
}
201+
202+
func (p *Proxy) lookupPath(ctx context.Context, page *config.Page, sourceHost string, backendUrl *url.URL, targetPath string) (string, humane.Error) {
203+
// Find the actual html document we are looking for
204+
searchPath := append([]string{""}, page.Proxy.SearchPath...)
205+
var wg sync.WaitGroup
206+
validPath := make(chan string, len(searchPath))
207+
208+
for _, lookupPath := range searchPath {
209+
// Probe each searchPath asynchronously
210+
wg.Add(1)
211+
go func() {
212+
defer wg.Done()
213+
214+
// Cache search path computation
215+
cacheKey := fmt.Sprintf("%s-%s-%s", sourceHost, targetPath, lookupPath)
216+
_, _ = p.group.Do(cacheKey, func() (interface{}, error) {
217+
// Probe the path
218+
testTarget := path.Clean(fmt.Sprintf("/%s/%s", targetPath, lookupPath))
219+
statusCode, err := p.probePath(ctx, backendUrl, testTarget)
220+
if err == nil && statusCode < http.StatusBadRequest {
221+
validPath <- testTarget
222+
return testTarget, nil
223+
}
224+
225+
return nil, humane.Wrap(err, "Unable to probe path", "Make sure the path exists and is accessible.")
226+
})
227+
}()
228+
}
229+
230+
go func() {
231+
wg.Wait()
232+
close(validPath)
233+
}()
234+
235+
if validPath, ok := <-validPath; ok {
236+
return validPath, nil
237+
}
238+
239+
return "", humane.New("no valid path found", "Make sure the path exists and is accessible.")
240+
}

0 commit comments

Comments
 (0)