Skip to content

Commit 450f8e3

Browse files
authored
Merge pull request #248 from Notifiarr/dn2_no_gorilla
Remove gorilla mux, replace with sodlib.
2 parents 6102f87 + b4a42c3 commit 450f8e3

14 files changed

Lines changed: 242 additions & 189 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ server {
5959
auth_request_set $auth_idnt $upstream_http_X_UserID;
6060
6161
proxy_set_header host $redirect_host;
62-
proxy_set_header x-api-key $remote_api_key;
62+
proxy_set_header X-Api-Key $remote_api_key;
6363
proxy_pass $server$request_uri;
6464
}
6565
@@ -68,7 +68,7 @@ server {
6868
proxy_pass_request_body off;
6969
proxy_set_header Content-Length "";
7070
proxy_set_header X-Original-URI $request_uri;
71-
proxy_set_header X-API-Key $incoming_api_key;
71+
proxy_set_header X-Api-Key $incoming_api_key;
7272
proxy_set_header X-Server $http_X_Server;
7373
proxy_pass $authproxy/auth;
7474
}

docs/api_docs.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const docTemplateapi = `{
1717
"paths": {
1818
"/auth": {
1919
"get": {
20-
"description": "Retrieve the environment for an API Key or Server ID. This endpoint is designed for auth proxy requests from Nginx.\nOne of X-Server, X-API-Key or X-Original-URI (with an api key in it) must be provided.",
20+
"description": "Retrieve the environment for an API Key or Server ID. This endpoint is designed for auth proxy requests from Nginx.\nOne of X-Server, X-Api-Key or X-Original-URI (with an api key in it) must be provided.",
2121
"tags": [
2222
"auth"
2323
],
@@ -38,7 +38,7 @@ const docTemplateapi = `{
3838
{
3939
"type": "string",
4040
"description": "User's API Key to route. May also be provided in X-Original-URI header.",
41-
"name": "X-API-Key",
41+
"name": "X-Api-Key",
4242
"in": "header"
4343
},
4444
{
@@ -56,7 +56,7 @@ const docTemplateapi = `{
5656
"type": "string",
5757
"description": "How long this information has been in the cache."
5858
},
59-
"X-API-Key": {
59+
"X-Api-Key": {
6060
"type": "string",
6161
"description": "API Key parsed from request."
6262
},
@@ -80,7 +80,7 @@ const docTemplateapi = `{
8080
"type": "string"
8181
},
8282
"headers": {
83-
"X-API-Key": {
83+
"X-Api-Key": {
8484
"type": "string",
8585
"description": "API Key parsed from request."
8686
}
@@ -108,7 +108,7 @@ const docTemplateapi = `{
108108
{
109109
"type": "string",
110110
"description": "Comma separated list of API keys to delete.",
111-
"name": "X-API-Keys",
111+
"name": "X-Api-Keys",
112112
"in": "header",
113113
"required": true
114114
}

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ go 1.26.1
44

55
require (
66
github.com/go-sql-driver/mysql v1.9.3
7-
github.com/gorilla/mux v1.8.1
87
github.com/prometheus/client_golang v1.23.2
98
github.com/swaggo/swag v1.16.6
109
golift.io/cache v1.0.1-0.20260406025202-1176587c97ab

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1
4242
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
4343
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
4444
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
45-
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
46-
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
4745
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
4846
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
4947
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=

pkg/exp/metrics.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,22 @@ func warmHTTPMetrics(metrics *Metrics) {
143143
metrics.HTTPResponse.WithLabelValues(strconv.Itoa(code))
144144
}
145145
}
146+
147+
// CountRequest increments the appropriate HTTP request and response metrics for a web request.
148+
func (m *Metrics) CountRequest(req *http.Request, statusCode string) {
149+
if m == nil {
150+
return
151+
}
152+
153+
m.HTTPRequests.WithLabelValues(HTTPEventTotal).Inc()
154+
155+
if req.Method == http.MethodDelete {
156+
m.HTTPRequests.WithLabelValues(HTTPEventDelete).Inc()
157+
}
158+
159+
if len(req.Header["X-Server"]) > 0 {
160+
m.HTTPRequests.WithLabelValues(HTTPEventXServer).Inc()
161+
}
162+
163+
m.HTTPResponse.WithLabelValues(statusCode).Inc()
164+
}

pkg/webserver/accesslog.go

Lines changed: 34 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ type captureWriter struct {
2424
}
2525

2626
func (c *captureWriter) WriteHeader(code int) {
27-
if c.status == 0 {
27+
if c.status == 0 { // cannot set it twice.
2828
c.status = code
2929
}
3030

3131
c.ResponseWriter.WriteHeader(code)
3232
}
3333

3434
func (c *captureWriter) Write(p []byte) (int, error) {
35-
if c.status == 0 {
35+
if c.status == 0 { // if you write without setting the status, it's a 200.
3636
c.status = http.StatusOK
3737
}
3838

@@ -42,37 +42,29 @@ func (c *captureWriter) Write(p []byte) (int, error) {
4242
return n, err //nolint:wrapcheck // delegate to underlying ResponseWriter
4343
}
4444

45-
func (c *captureWriter) statusCode() int {
45+
func (c *captureWriter) statusCode() string {
4646
if c.status == 0 {
47-
return http.StatusOK
48-
}
49-
50-
return c.status
51-
}
52-
53-
// Get returns the first value for a response header field. key must already be in
54-
// canonical form (http.CanonicalHeaderKey). Unlike Header.Get it does not allocate
55-
// or re-canonicalize key on each call.
56-
func (c *captureWriter) Get(key string) string {
57-
if v := c.Header()[key]; len(v) > 0 {
58-
return v[0]
47+
return "200"
5948
}
6049

61-
return ""
50+
return strconv.Itoa(c.status)
6251
}
6352

64-
//nolint:gochecknoglobals // one pool per process for hot-path access log strings.Builder reuse
65-
var alBuilder = sync.Pool{New: func() any { return &strings.Builder{} }}
66-
67-
// accessLogWrap writes one Apache-style line per request to dst (same field order as the former alFmt).
68-
func accessLogWrap(next http.Handler, dst io.Writer) http.Handler {
69-
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
70-
capture := &captureWriter{ResponseWriter: w, start: time.Now()}
53+
// accessLogWrap writes one Apache-style line per request to dst (same field order as the former alFmt)
54+
// and records HTTP request/response Prometheus counters when metrics is non-nil.
55+
func (s *server) accessLogWrap(next http.Handler, dst io.Writer) http.Handler {
56+
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
57+
capture := &captureWriter{ResponseWriter: resp, start: time.Now()}
7158
next.ServeHTTP(capture, req)
7259
capture.writeAccessLogLine(req, dst)
60+
// Update Prometheus metrics for the request.
61+
s.metrics.CountRequest(req, capture.statusCode())
7362
})
7463
}
7564

65+
//nolint:gochecknoglobals // one pool per process for hot-path access log strings.Builder reuse
66+
var alBuilder = sync.Pool{New: func() any { return &strings.Builder{} }}
67+
7668
func (c *captureWriter) writeAccessLogLine(req *http.Request, dst io.Writer) {
7769
//nolint:forcetypeassert
7870
builder := alBuilder.Get().(*strings.Builder) // Get a string buffer:
@@ -85,6 +77,7 @@ func (c *captureWriter) writeAccessLogLine(req *http.Request, dst io.Writer) {
8577
}
8678

8779
func (c *captureWriter) writeAccessLogLinePrefix(builder *strings.Builder, req *http.Request) {
80+
respHeader := c.Header()
8881
// %V
8982
builder.WriteString(req.Host)
9083
builder.WriteByte(' ')
@@ -93,10 +86,10 @@ func (c *captureWriter) writeAccessLogLinePrefix(builder *strings.Builder, req *
9386
builder.WriteByte(' ')
9487
// "%{X-Username}o"
9588
builder.WriteByte('"')
96-
builder.WriteString(c.Get("X-Username"))
89+
builder.WriteString(getHeader(respHeader, HeaderXUsername))
9790
builder.WriteString("\" ")
9891
// %{X-UserID}o
99-
builder.WriteString(c.Get("X-Userid"))
92+
builder.WriteString(getHeader(respHeader, HeaderXUserid))
10093
builder.WriteByte(' ')
10194
// %t — [02/Jan/2006:15:04:05 -0700]
10295
builder.WriteByte('[')
@@ -115,7 +108,7 @@ func (c *captureWriter) writeAccessLogLinePrefix(builder *strings.Builder, req *
115108

116109
builder.WriteString(" HTTP/1.1\" ")
117110
// %>s
118-
builder.WriteString(strconv.Itoa(c.statusCode()))
111+
builder.WriteString(c.statusCode())
119112
builder.WriteByte(' ')
120113
// %b — response body size (0 when none).
121114
builder.WriteString(strconv.FormatInt(c.size, 10))
@@ -130,28 +123,33 @@ func (c *captureWriter) writeAccessLogLinePrefix(builder *strings.Builder, req *
130123
}
131124

132125
func (c *captureWriter) writeAccessLogLineTail(builder *strings.Builder, req *http.Request) {
126+
respHeader := c.Header()
133127
builder.WriteString(" req:")
134128
// %{ms}T — elapsed milliseconds (same as apache-logformat request duration).
135129
builder.WriteString(strconv.FormatInt(time.Since(c.start).Milliseconds(), 10))
136130
builder.WriteString("ms age:")
137-
builder.WriteString(c.Get("Age"))
131+
builder.WriteString(getHeader(respHeader, HeaderAge))
138132
builder.WriteString(" env:")
139-
builder.WriteString(c.Get("X-Environment"))
133+
builder.WriteString(getHeader(respHeader, HeaderEnvironment))
140134
builder.WriteString(" key:")
141135

142-
masked, keyLenStr := c.maskedAPIKeyFromResponse()
136+
masked, keyLenStr := maskedAPIKeyForLog(req, respHeader)
143137
builder.WriteString(masked)
144138
builder.WriteByte('(')
145139
builder.WriteString(keyLenStr)
146140
builder.WriteString(") \"srv:")
147-
builder.WriteString(req.Header.Get("X-Server"))
141+
builder.WriteString(getHeader(req.Header, HeaderXServer))
148142
builder.WriteString("\"\n")
149143
}
150144

151-
// maskedAPIKeyFromResponse returns maskAPIKey(w X-Api-Key) for the access log, or ("", "")
152-
// when the handler did not set that response header.
153-
func (c *captureWriter) maskedAPIKeyFromResponse() (string, string) {
154-
key := c.Get("X-Api-Key")
145+
// maskedAPIKeyForLog returns maskAPIKey(X-Api-Key) from the response, else from the parsed request
146+
// context, or ("", "") if neither is set.
147+
func maskedAPIKeyForLog(req *http.Request, resp http.Header) (string, string) {
148+
key := getHeader(resp, HeaderXAPIKey)
149+
if key == "" {
150+
key = apiKeyFromRequest(req)
151+
}
152+
155153
if key == "" {
156154
return "", ""
157155
}
@@ -164,7 +162,7 @@ func (c *captureWriter) maskedAPIKeyFromResponse() (string, string) {
164162
// If the path has fewer than keyPosition+1 segments, it returns the full path (still without query).
165163
// When X-Original-Uri is missing, empty, or only a query string, it returns "".
166164
func RefererPathForLog(header http.Header) string {
167-
pathPart, _, _ := strings.Cut(header.Get("X-Original-Uri"), "?")
165+
pathPart, _, _ := strings.Cut(getHeader(header, HeaderXOriginalURI), "?")
168166
if pathPart == "" {
169167
return ""
170168
}
@@ -189,7 +187,7 @@ func RefererPathForLog(header http.Header) string {
189187

190188
// ClientIPForLog returns the client IP for access logs (same rules as the former fixForwardedFor middleware).
191189
func ClientIPForLog(req *http.Request) string {
192-
forwarded := req.Header.Get("X-Forwarded-For")
190+
forwarded := getHeader(req.Header, HeaderXForwardedFor)
193191
if forwarded == "" {
194192
host, _, err := net.SplitHostPort(req.RemoteAddr)
195193
if err != nil {

pkg/webserver/accesslog_internal_test.go

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ func TestResponseWriter(t *testing.T) {
2828
rec := httptest.NewRecorder()
2929
capWriter := &captureWriter{ResponseWriter: rec}
3030

31-
if capWriter.statusCode() != http.StatusOK {
32-
t.Fatalf("statusCode before any write = %d, want 200", capWriter.statusCode())
31+
if capWriter.statusCode() != "200" {
32+
t.Fatalf("statusCode before any write = %s, want 200", capWriter.statusCode())
3333
}
3434

3535
capWriter.WriteHeader(http.StatusTeapot)
@@ -57,8 +57,8 @@ func TestResponseWriter(t *testing.T) {
5757
t.Fatalf("size = %d, want 2", capWriter.size)
5858
}
5959

60-
if capWriter.statusCode() != http.StatusTeapot {
61-
t.Fatalf("statusCode = %d, want %d", capWriter.statusCode(), http.StatusTeapot)
60+
if capWriter.statusCode() != "418" {
61+
t.Fatalf("statusCode = %s, want 418", capWriter.statusCode())
6262
}
6363
}
6464

@@ -77,8 +77,8 @@ func TestResponseWriter_WriteImpliesOK(t *testing.T) {
7777
t.Fatalf("Write without WriteHeader: status = %d, want 200", capWriter.status)
7878
}
7979

80-
if capWriter.statusCode() != http.StatusOK {
81-
t.Fatalf("statusCode = %d, want 200", capWriter.statusCode())
80+
if capWriter.statusCode() != "200" {
81+
t.Fatalf("statusCode = %s, want 200", capWriter.statusCode())
8282
}
8383
}
8484

@@ -91,16 +91,16 @@ func TestWriteAccessLogLine(t *testing.T) {
9191
req.RequestURI = "/p?q=1"
9292
req.RemoteAddr = "192.0.2.1:9999"
9393
req.Header.Set("User-Agent", "test-agent/1")
94-
req.Header.Set("X-Server", "srv-99")
95-
req.Header.Set("X-Original-Uri", "/api/v1/route/method/"+TestAccessLogAPIKey)
94+
req.Header.Set(HeaderXServer, "srv-99")
95+
req.Header.Set(HeaderXOriginalURI, "/api/v1/route/method/"+TestAccessLogAPIKey)
9696

9797
rec := httptest.NewRecorder()
9898
capWriter := &captureWriter{ResponseWriter: rec}
9999
capWriter.start = time.Now().Add(-elapsed)
100-
capWriter.Header().Set("X-Username", "alice")
101-
capWriter.Header().Set("X-Userid", "1001")
102-
capWriter.Header().Set("Age", "60")
103-
capWriter.Header().Set("X-Environment", "live")
100+
capWriter.Header().Set(HeaderXUsername, "alice")
101+
capWriter.Header().Set(HeaderXUserid, "1001")
102+
capWriter.Header().Set(HeaderAge, "60")
103+
capWriter.Header().Set(HeaderEnvironment, "live")
104104
capWriter.WriteHeader(http.StatusOK)
105105
_, _ = capWriter.Write([]byte("body"))
106106

@@ -168,12 +168,12 @@ func TestWriteAccessLogLine_MaskedKeyFromResponseHeader(t *testing.T) {
168168

169169
start := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
170170
req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "http://x/", nil)
171-
req.Header.Set("X-Server", "")
171+
req.Header.Set(HeaderXServer, "")
172172

173173
rec := httptest.NewRecorder()
174174
capWriter := &captureWriter{ResponseWriter: rec}
175175
capWriter.start = start
176-
capWriter.Header().Set("X-Api-Key", TestAccessLogAPIKey)
176+
capWriter.Header().Set(HeaderXAPIKey, TestAccessLogAPIKey)
177177

178178
builder := &strings.Builder{}
179179

@@ -210,19 +210,20 @@ func TestAccessLogWrap(t *testing.T) {
210210

211211
var dst bytes.Buffer
212212

213-
handler := accessLogWrap(http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) {
214-
resp.Header().Set("X-Username", "wrap-user")
215-
resp.Header().Set("X-Userid", "55")
216-
resp.Header().Set("Age", "3")
217-
resp.Header().Set("X-Environment", "dev")
213+
srv := &server{}
214+
handler := srv.accessLogWrap(http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) {
215+
resp.Header().Set(HeaderXUsername, "wrap-user")
216+
resp.Header().Set(HeaderXUserid, "55")
217+
resp.Header().Set(HeaderAge, "3")
218+
resp.Header().Set(HeaderEnvironment, "dev")
218219
resp.WriteHeader(http.StatusNoContent)
219220
}), &dst)
220221

221222
req := httptest.NewRequestWithContext(context.Background(), http.MethodGet, "http://proxy.test/auth", nil)
222223
req.RequestURI = "/auth"
223224
req.RemoteAddr = "198.51.100.2:4444"
224-
req.Header.Set("X-Forwarded-For", "203.0.113.5")
225-
req.Header.Set("X-Server", "discord-srv")
225+
req.Header.Set(HeaderXForwardedFor, "203.0.113.5")
226+
req.Header.Set(HeaderXServer, "discord-srv")
226227
req.Header.Set("User-Agent", "ua-wrap")
227228

228229
rec := httptest.NewRecorder()
@@ -255,8 +256,9 @@ func TestAccessLogWrap_MaskedKeyFromResponseHeader(t *testing.T) {
255256

256257
var dst bytes.Buffer
257258

258-
handler := accessLogWrap(http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) {
259-
resp.Header().Set("X-Api-Key", TestAccessLogAPIKey)
259+
srv := &server{}
260+
handler := srv.accessLogWrap(http.HandlerFunc(func(resp http.ResponseWriter, _ *http.Request) {
261+
resp.Header().Set(HeaderXAPIKey, TestAccessLogAPIKey)
260262
resp.WriteHeader(http.StatusUnauthorized)
261263
}), &dst)
262264

pkg/webserver/accesslog_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestRefererPathForLog(t *testing.T) {
5151
t.Parallel()
5252

5353
h := http.Header{}
54-
h.Set("X-Original-Uri", testCase.origURI)
54+
h.Set(webserver.HeaderXOriginalURI, testCase.origURI)
5555

5656
if got := webserver.RefererPathForLog(h); got != testCase.want {
5757
t.Fatalf("RefererPathForLog = %q, want %q (X-Original-Uri=%q)", got, testCase.want, testCase.origURI)
@@ -74,7 +74,7 @@ func TestClientIPForLog(t *testing.T) {
7474
t.Fatalf("ClientIPForLog = %q, want 192.0.2.1", got)
7575
}
7676

77-
req.Header.Set("X-Forwarded-For", " 203.0.113.9 , 198.51.100.1 ")
77+
req.Header.Set(webserver.HeaderXForwardedFor, " 203.0.113.9 , 198.51.100.1 ")
7878

7979
if got := webserver.ClientIPForLog(req); got != "203.0.113.9" {
8080
t.Fatalf("ClientIPForLog with XFF = %q, want 203.0.113.9", got)

0 commit comments

Comments
 (0)