Skip to content

Commit 557a499

Browse files
feat: redact Authorization header when using debug option
1 parent 76d63c3 commit 557a499

File tree

3 files changed

+168
-1
lines changed

3 files changed

+168
-1
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package debugmiddleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httputil"
6+
"strings"
7+
)
8+
9+
// For the time being these type definitions are duplicated here so that we can
10+
// test this file in a non-generated context.
11+
type (
12+
Middleware = func(*http.Request, MiddlewareNext) (*http.Response, error)
13+
MiddlewareNext = func(*http.Request) (*http.Response, error)
14+
)
15+
16+
const redactedPlaceholder = "<REDACTED>"
17+
18+
// DebugMiddleware returns a middleware that logs HTTP requests and responses.
19+
//
20+
// logWriter is log.Default() under most circumstances, but made low level so we
21+
// can more easily inject a buffer to check in tests.
22+
func DebugMiddleware(logger interface{ Printf(string, ...any) }) Middleware {
23+
return func(req *http.Request, mn MiddlewareNext) (*http.Response, error) {
24+
if reqBytes, err := httputil.DumpRequest(redactRequest(req), true); err == nil {
25+
logger.Printf("Request Content:\n%s\n", reqBytes)
26+
}
27+
28+
resp, err := mn(req)
29+
if err != nil {
30+
return resp, err
31+
}
32+
33+
if respBytes, err := httputil.DumpResponse(resp, true); err == nil {
34+
logger.Printf("Response Content:\n%s\n", respBytes)
35+
}
36+
37+
return resp, err
38+
}
39+
}
40+
41+
// redactRequest redacts sensitive information from the request for logging
42+
// purposes. If redaction is necessary, the request is cloned before mutating
43+
// the original and that clone is returned. As a small optimization, the
44+
// original is request is returned unchanged if no redaction is necessary.
45+
func redactRequest(req *http.Request) *http.Request {
46+
if auth := req.Header.Get("Authorization"); auth != "" {
47+
req = req.Clone(req.Context())
48+
49+
// In case we're using something like a bearer token (e.g. `Bearer
50+
// <my_token>`), keep the `Bearer` part for more debugging
51+
// information.
52+
if authKind, _, ok := strings.Cut(auth, " "); ok {
53+
req.Header.Set("Authorization", authKind+" "+redactedPlaceholder)
54+
} else {
55+
req.Header.Set("Authorization", redactedPlaceholder)
56+
}
57+
}
58+
59+
return req
60+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package debugmiddleware
2+
3+
import (
4+
"bytes"
5+
"log"
6+
"net/http"
7+
"net/http/httptest"
8+
"strings"
9+
"testing"
10+
)
11+
12+
func TestDebugMiddleware(t *testing.T) {
13+
t.Parallel()
14+
15+
setup := func() (Middleware, *bytes.Buffer) {
16+
var logBuf bytes.Buffer
17+
return DebugMiddleware(log.New(&logBuf, "", 0)), &logBuf
18+
}
19+
20+
t.Run("DoesNotRedactMostHeaders", func(t *testing.T) {
21+
t.Parallel()
22+
23+
middleware, logBuf := setup()
24+
25+
const stainlessUserAgent = "Stainless"
26+
27+
req := httptest.NewRequest("GET", "https://example.com", nil)
28+
req.Header.Set("User-Agent", stainlessUserAgent)
29+
30+
var nextMiddlewareRan bool
31+
middleware(req, func(req *http.Request) (*http.Response, error) {
32+
nextMiddlewareRan = true
33+
34+
// The request sent down through middleware shouldn't be mutated.
35+
if req.Header.Get("User-Agent") != stainlessUserAgent {
36+
t.Error("expected original request to be unmodified")
37+
}
38+
39+
return &http.Response{}, nil
40+
})
41+
42+
if !nextMiddlewareRan {
43+
t.Error("expected next middleware to have been run")
44+
}
45+
46+
if !strings.Contains(logBuf.String(), "User-Agent: "+stainlessUserAgent) {
47+
t.Error("expected logged request headers to include `User-Agent: Stainless`")
48+
}
49+
})
50+
51+
const secretToken = "secret-token"
52+
53+
t.Run("RedactsAuthorizationHeader", func(t *testing.T) {
54+
t.Parallel()
55+
56+
middleware, logBuf := setup()
57+
58+
req := httptest.NewRequest("GET", "https://example.com", nil)
59+
req.Header.Set("Authorization", secretToken)
60+
61+
var nextMiddlewareRan bool
62+
middleware(req, func(req *http.Request) (*http.Response, error) {
63+
nextMiddlewareRan = true
64+
65+
// The request sent down through middleware shouldn't be mutated.
66+
if req.Header.Get("Authorization") != secretToken {
67+
t.Error("expected original request to be unmodified")
68+
}
69+
70+
return &http.Response{}, nil
71+
})
72+
73+
if !nextMiddlewareRan {
74+
t.Error("expected next middleware to have been run")
75+
}
76+
77+
if !strings.Contains(logBuf.String(), "Authorization: "+redactedPlaceholder) {
78+
t.Error("expected authorization header to be redacted")
79+
}
80+
})
81+
82+
t.Run("RedactsOnlySecretInAuthorizationHeader", func(t *testing.T) {
83+
t.Parallel()
84+
85+
middleware, logBuf := setup()
86+
87+
req := httptest.NewRequest("GET", "https://example.com", nil)
88+
req.Header.Set("Authorization", "Bearer "+secretToken)
89+
90+
var nextMiddlewareRan bool
91+
middleware(req, func(req *http.Request) (*http.Response, error) {
92+
nextMiddlewareRan = true
93+
94+
return &http.Response{}, nil
95+
})
96+
97+
if !nextMiddlewareRan {
98+
t.Error("expected next middleware to have been run")
99+
}
100+
101+
if !strings.Contains(logBuf.String(), "Authorization: Bearer "+redactedPlaceholder) {
102+
t.Error("expected authorization header to be redacted")
103+
}
104+
})
105+
}

pkg/cmd/flagoptions.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"bytes"
55
"encoding/json"
66
"io"
7+
"log"
78
"mime/multipart"
89
"os"
910

1011
"github.com/stainless-api/stainless-api-cli/internal/apiform"
1112
"github.com/stainless-api/stainless-api-cli/internal/apiquery"
13+
"github.com/stainless-api/stainless-api-cli/internal/debugmiddleware"
1214
"github.com/stainless-api/stainless-api-cli/internal/requestflag"
1315
"github.com/stainless-api/stainless-api-go/option"
1416

@@ -30,7 +32,7 @@ func flagOptions(
3032
) ([]option.RequestOption, error) {
3133
var options []option.RequestOption
3234
if cmd.Bool("debug") {
33-
options = append(options, debugMiddlewareOption)
35+
options = append(options, option.WithMiddleware(debugmiddleware.DebugMiddleware(log.Default())))
3436
}
3537

3638
queries := make(map[string]any)

0 commit comments

Comments
 (0)