├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── debughttp.go ├── debughttp_test.go ├── examples_test.go ├── go.mod └── go.sum /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Github Actions build for rclone 3 | # -*- compile-command: "yamllint -f parsable build.yml" -*- 4 | 5 | name: build 6 | 7 | # Trigger the workflow on push or pull request 8 | on: 9 | push: 10 | branches: 11 | - '*' 12 | tags: 13 | - '*' 14 | pull_request: 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | go: ['1.19', '1.18', '1.17'] 23 | name: Go ${{ matrix.go }} 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup Go 29 | uses: actions/setup-go@v1 30 | with: 31 | go-version: ${{ matrix.go }} 32 | 33 | - name: Version 34 | run: go version 35 | 36 | - name: Fetch dependencies 37 | run: go get -t -v ./... 38 | 39 | - name: Build 40 | run: go install 41 | 42 | - name: Run tests 43 | run: go test -v 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Emacs save files 18 | *~ 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 by Nick Craig-Wood http://www.craig-wood.com/nick/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [rclone logo](https://rclone.org/) 2 | 3 | [![Go Docs](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/rclone/debughttp) 4 | [![Build Status](https://github.com/rclone/debughttp/workflows/build/badge.svg)](https://github.com/rclone/debughttp/actions?query=workflow%3Abuild) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/rclone/debughttp)](https://goreportcard.com/report/github.com/rclone/debughttp) 6 | 7 | # debughttp 8 | 9 | This is a go package to dump HTTP requests and responses 10 | 11 | ## Install 12 | 13 | Install like this 14 | 15 | go get github.com/rclone/debughttp 16 | 17 | and this will build the binary in `$GOPATH/bin`. 18 | 19 | ## Usage 20 | 21 | See the [full docs](https://pkg.go.dev/github.com/rclone/debughttp) 22 | or read on for a quickstart 23 | Instead of using http.Get or client.Get, use this 24 | 25 | ```go 26 | import github.com/rclone/debughttp 27 | 28 | / Make a client with the defaults which dump headers to log.Printf 29 | client := debughttp.NewClient(nil) 30 | 31 | // Now use the client, eg 32 | resp, err := client.Get("http://example.com") 33 | ``` 34 | 35 | This will log something like this 36 | 37 | ``` 38 | 2020/05/03 16:06:03 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 39 | 2020/05/03 16:06:03 HTTP REQUEST (req 0xc00022a300) 40 | 2020/05/03 16:06:03 GET / HTTP/1.1 41 | Host: example.com 42 | User-Agent: Go-http-client/1.1 43 | Accept-Encoding: gzip 44 | 45 | 2020/05/03 16:06:03 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 46 | 2020/05/03 16:06:03 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 47 | 2020/05/03 16:06:03 HTTP RESPONSE (req 0xc00022a300) 48 | 2020/05/03 16:06:03 HTTP/1.1 200 OK 49 | Accept-Ranges: bytes 50 | Age: 518408 51 | Cache-Control: max-age=604800 52 | Content-Type: text/html; charset=UTF-8 53 | Date: Sun, 03 May 2020 15:06:03 GMT 54 | Etag: "3147526947" 55 | Expires: Sun, 10 May 2020 15:06:03 GMT 56 | Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT 57 | Server: ECS (nyb/1D2A) 58 | Vary: Accept-Encoding 59 | X-Cache: HIT 60 | 61 | 2020/05/03 16:06:03 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 62 | ``` 63 | 64 | If you want to see the bodies of the transactions use this 65 | 66 | ```go 67 | // Make a client with the defaults which dump headers and bodies to log.Printf 68 | client := debughttp.NewClient(debughttp.DumpBodyOptions) 69 | ``` 70 | 71 | Note that this redacts authorization headers by default. 72 | 73 | For more info see [the full documentation](https://pkg.go.dev/github.com/rclone/debughttp). 74 | 75 | ## License 76 | 77 | This is free software under the terms of the MIT license (check the 78 | LICENSE file included in this package). 79 | 80 | This code was originally part of the rclone binary but was factored out to be of wider use. 81 | 82 | ## Contact and support 83 | 84 | The project website is at: 85 | 86 | - https://github.com/rclone/debughttp 87 | 88 | There you can file bug reports, ask for help or contribute patches. 89 | -------------------------------------------------------------------------------- /debughttp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package debughttp provides an http Transport or Client which can be 3 | used for tracing HTTP requests for debugging purposes. 4 | 5 | Quickstart 6 | 7 | This can be used for a quick bit of debugging. 8 | 9 | Instead of using http.Get or client.Get, use this 10 | 11 | // Make a client with the defaults which dump headers to log.Printf 12 | client := debughttp.NewClient(nil) 13 | 14 | // Now use the client, eg 15 | resp, err := client.Get("http://example.com") 16 | 17 | This will log something like this 18 | 19 | 2020/05/03 16:06:03 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 20 | 2020/05/03 16:06:03 HTTP REQUEST (req 0xc00022a300) 21 | 2020/05/03 16:06:03 GET / HTTP/1.1 22 | Host: example.com 23 | User-Agent: Go-http-client/1.1 24 | Accept-Encoding: gzip 25 | 26 | 2020/05/03 16:06:03 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 27 | 2020/05/03 16:06:03 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 28 | 2020/05/03 16:06:03 HTTP RESPONSE (req 0xc00022a300) 29 | 2020/05/03 16:06:03 HTTP/1.1 200 OK 30 | Accept-Ranges: bytes 31 | Age: 518408 32 | Cache-Control: max-age=604800 33 | Content-Type: text/html; charset=UTF-8 34 | Date: Sun, 03 May 2020 15:06:03 GMT 35 | Etag: "3147526947" 36 | Expires: Sun, 10 May 2020 15:06:03 GMT 37 | Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT 38 | Server: ECS (nyb/1D2A) 39 | Vary: Accept-Encoding 40 | X-Cache: HIT 41 | 42 | 2020/05/03 16:06:03 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 43 | 44 | If you want to see the bodies of the transactions use this 45 | 46 | // Make a client with the defaults which dump headers and bodies to log.Printf 47 | client := debughttp.NewClient(debughttp.DumpBodyOptions) 48 | 49 | Note that this redacts authorization headers by default. 50 | 51 | Fuller integration 52 | 53 | If you want more control over what is logged and what isn't logged 54 | then you can use the Options struct, eg 55 | 56 | client := debughttp.NewClient(&debughttp.Options{ 57 | Flags: debughttp.DumpRequests|debughttp.DumpAuth, 58 | } 59 | 60 | If you are integrating this with code which has its own logging system 61 | then you will want to pass in the Logf parameter to control where 62 | the logs are sent. 63 | 64 | Every Go library which does HTTP transactions on your behalf should 65 | take an http.Client or allow the setting of an http.Transport 66 | replacement. (If you find one which doesn't, then report an issue!) 67 | 68 | To create a new Transport use the NewDefault function to base one 69 | off the default transport or the New function to base one off an 70 | existing transport. 71 | 72 | This means that you can use this library for debugging other people's 73 | code. For example this is how you add this library to the AWS SDK 74 | 75 | client := debughttp.NewClient(nil) 76 | awsConfig := aws.NewConfig(). 77 | WithCredentials(cred). 78 | WithHTTPClient(fshttp.NewClient(fs.Config)) 79 | 80 | If you do this you can see exactly what requests are sent to and from 81 | AWS. 82 | 83 | Warnings 84 | 85 | If dumping bodies is enabled the bodies are held in memory so large 86 | requests and responses can use a lot of memory. 87 | 88 | The Accept-Encoding as shown may not be correct in the Request and 89 | the Response may not show Content-Encoding if the Go standard 90 | libraries auto gzip encoding was in effect. In this case the body of 91 | the request will be gunzipped before showing it. 92 | */ 93 | package debughttp 94 | 95 | import ( 96 | "bytes" 97 | "log" 98 | "net/http" 99 | "net/http/httputil" 100 | "reflect" 101 | ) 102 | 103 | var ( 104 | // Default separators for request and responses 105 | SeparatorReq = ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" 106 | SeparatorResp = "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 107 | ) 108 | 109 | // DumpFlags describes the Dump options in force 110 | type DumpFlags int 111 | 112 | // DumpFlags definitions 113 | const ( 114 | DumpHeaders DumpFlags = 1 << iota // dump just the http headers 115 | DumpBodies // dump the bodies also 116 | DumpRequests // dump all the headers and the request bodies but not the response bodies 117 | DumpResponses // dump all the headers and the response bodies but not the request bodies 118 | DumpAuth // dump the auth instead of redacting it 119 | ) 120 | 121 | // Options controls the configuration of the HTTP debugging 122 | type Options struct { 123 | Flags DumpFlags // Which parts of the HTTP transaction we are dumping 124 | Logf func(format string, v ...interface{}) // Where to log the dumped transactions - defaults to log.Printf if not set 125 | Auth [][]byte // which headers we are treating as Auth to redact - defaults to Auth if not set 126 | } 127 | 128 | // Default options if nil is passed in to New or NewDefault or NewClient 129 | var DefaultOptions = Options{ 130 | Flags: DumpHeaders, 131 | Logf: log.Printf, 132 | Auth: Auth, 133 | } 134 | 135 | // DumpBodyOptions is an easy set of options for dumping bodies 136 | var DumpBodyOptions = Options{ 137 | Flags: DumpBodies, 138 | Logf: log.Printf, 139 | Auth: Auth, 140 | } 141 | 142 | // Auth is the headers which we redact if DumpAuth is not set in Options 143 | var Auth = [][]byte{ 144 | []byte("Authorization: "), 145 | []byte("X-Auth-Token: "), 146 | } 147 | 148 | // Transport wraps an *http.Transport and logs requests and responses 149 | // 150 | // Create one with New, NewDefault or NewClient - don't use directly 151 | type Transport struct { 152 | *http.Transport 153 | opt Options 154 | } 155 | 156 | // New wraps the http.Transport passed in and logs all 157 | // round trips according to the Flags in opt 158 | func New(opt *Options, transport *http.Transport) *Transport { 159 | if opt == nil { 160 | opt = &DefaultOptions 161 | } 162 | t := &Transport{ 163 | Transport: transport, 164 | opt: *opt, 165 | } 166 | if t.opt.Logf == nil { 167 | t.opt.Logf = log.Printf 168 | } 169 | if t.opt.Auth == nil { 170 | t.opt.Auth = Auth 171 | } 172 | return t 173 | } 174 | 175 | // setDefaults for a from b 176 | // 177 | // Copy the public members from b to a. We can't just use a struct 178 | // copy as Transport contains a private mutex. 179 | func setDefaults(a, b interface{}) { 180 | pt := reflect.TypeOf(a) 181 | t := pt.Elem() 182 | va := reflect.ValueOf(a).Elem() 183 | vb := reflect.ValueOf(b).Elem() 184 | for i := 0; i < t.NumField(); i++ { 185 | aField := va.Field(i) 186 | // Set a from b if it is public 187 | if aField.CanSet() { 188 | bField := vb.Field(i) 189 | aField.Set(bField) 190 | } 191 | } 192 | } 193 | 194 | // NewDefault returns an http.RoundTripper based off 195 | // http.DefaultTransport which will log the HTTP transactions as 196 | // directed in opt 197 | // 198 | // If opt is nil then DefaultOptions is used 199 | func NewDefault(opt *Options) *Transport { 200 | // Start with a sensible set of defaults then override. 201 | // This also means we get new stuff when it gets added to go 202 | t := new(http.Transport) 203 | setDefaults(t, http.DefaultTransport.(*http.Transport)) 204 | 205 | // Wrap that http.Transport in our own transport 206 | return New(opt, t) 207 | } 208 | 209 | // NewClient returns an http.Client based off a transport which will 210 | // log the HTTP transactions as directed in opt 211 | func NewClient(opt *Options) *http.Client { 212 | client := &http.Client{ 213 | Transport: NewDefault(opt), 214 | } 215 | return client 216 | } 217 | 218 | // cleanAuth gets rid of one authBuf header within the first 4k 219 | func cleanAuth(buf, authBuf []byte) []byte { 220 | // Find how much buffer to check 221 | n := 4096 222 | if len(buf) < n { 223 | n = len(buf) 224 | } 225 | // See if there is an Authorization: header 226 | i := bytes.Index(buf[:n], authBuf) 227 | if i < 0 { 228 | return buf 229 | } 230 | i += len(authBuf) 231 | // Overwrite the next 4 chars with 'X' 232 | for j := 0; i < len(buf) && j < 4; j++ { 233 | if buf[i] == '\n' { 234 | break 235 | } 236 | buf[i] = 'X' 237 | i++ 238 | } 239 | // Snip out to the next '\n' 240 | j := bytes.IndexByte(buf[i:], '\n') 241 | if j < 0 { 242 | return buf[:i] 243 | } 244 | n = copy(buf[i:], buf[i+j:]) 245 | return buf[:i+n] 246 | } 247 | 248 | // cleanAuths gets rid of all the possible Auth headers 249 | func (t *Transport) cleanAuths(buf []byte) []byte { 250 | for _, authBuf := range t.opt.Auth { 251 | buf = cleanAuth(buf, authBuf) 252 | } 253 | return buf 254 | } 255 | 256 | // RoundTrip implements the RoundTripper interface. 257 | func (t *Transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { 258 | // Logf request 259 | if t.opt.Flags&(DumpHeaders|DumpBodies|DumpAuth|DumpRequests|DumpResponses) != 0 { 260 | t.opt.Logf("%s", SeparatorReq) 261 | t.opt.Logf("%s (req %p)", "HTTP REQUEST", req) 262 | buf, derr := httputil.DumpRequestOut(req, t.opt.Flags&(DumpBodies|DumpRequests) != 0) 263 | if derr != nil { 264 | t.opt.Logf("Dump request failed: %v", derr) 265 | } else { 266 | if t.opt.Flags&DumpAuth == 0 { 267 | buf = t.cleanAuths(buf) 268 | } 269 | t.opt.Logf("%s", string(buf)) 270 | } 271 | t.opt.Logf("%s", SeparatorReq) 272 | } 273 | // Do round trip 274 | resp, err = t.Transport.RoundTrip(req) 275 | // Logf response 276 | if t.opt.Flags&(DumpHeaders|DumpBodies|DumpAuth|DumpRequests|DumpResponses) != 0 { 277 | t.opt.Logf("%s", SeparatorResp) 278 | t.opt.Logf("%s (req %p)", "HTTP RESPONSE", req) 279 | if err != nil { 280 | t.opt.Logf("HTTP request failed: %v", err) 281 | } else { 282 | buf, derr := httputil.DumpResponse(resp, t.opt.Flags&(DumpBodies|DumpResponses) != 0) 283 | if derr != nil { 284 | t.opt.Logf("Dump response failed: %v", derr) 285 | } else { 286 | t.opt.Logf("%s", string(buf)) 287 | } 288 | } 289 | t.opt.Logf("%s", SeparatorResp) 290 | } 291 | return resp, err 292 | } 293 | -------------------------------------------------------------------------------- /debughttp_test.go: -------------------------------------------------------------------------------- 1 | package debughttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | // Check transport implements the interface 17 | var _ http.RoundTripper = (*Transport)(nil) 18 | 19 | // returns the "%p" representation of the thing passed in 20 | func ptr(p interface{}) string { 21 | return fmt.Sprintf("%p", p) 22 | } 23 | 24 | func TestSetDefaults(t *testing.T) { 25 | old := http.DefaultTransport.(*http.Transport) 26 | newT := new(http.Transport) 27 | setDefaults(newT, old) 28 | // Can't use assert.Equal or reflect.DeepEqual for this as it has functions in 29 | // Check functions by comparing the "%p" representations of them 30 | assert.Equal(t, ptr(old.Proxy), ptr(newT.Proxy), "when checking .Proxy") 31 | assert.Equal(t, ptr(old.DialContext), ptr(newT.DialContext), "when checking .DialContext") 32 | // Check the other public fields 33 | assert.Equal(t, ptr(old.Dial), ptr(newT.Dial), "when checking .Dial") 34 | assert.Equal(t, ptr(old.DialTLS), ptr(newT.DialTLS), "when checking .DialTLS") 35 | assert.Equal(t, old.TLSClientConfig, newT.TLSClientConfig, "when checking .TLSClientConfig") 36 | assert.Equal(t, old.TLSHandshakeTimeout, newT.TLSHandshakeTimeout, "when checking .TLSHandshakeTimeout") 37 | assert.Equal(t, old.DisableKeepAlives, newT.DisableKeepAlives, "when checking .DisableKeepAlives") 38 | assert.Equal(t, old.DisableCompression, newT.DisableCompression, "when checking .DisableCompression") 39 | assert.Equal(t, old.MaxIdleConns, newT.MaxIdleConns, "when checking .MaxIdleConns") 40 | assert.Equal(t, old.MaxIdleConnsPerHost, newT.MaxIdleConnsPerHost, "when checking .MaxIdleConnsPerHost") 41 | assert.Equal(t, old.IdleConnTimeout, newT.IdleConnTimeout, "when checking .IdleConnTimeout") 42 | assert.Equal(t, old.ResponseHeaderTimeout, newT.ResponseHeaderTimeout, "when checking .ResponseHeaderTimeout") 43 | assert.Equal(t, old.ExpectContinueTimeout, newT.ExpectContinueTimeout, "when checking .ExpectContinueTimeout") 44 | assert.Equal(t, old.TLSNextProto, newT.TLSNextProto, "when checking .TLSNextProto") 45 | assert.Equal(t, old.MaxResponseHeaderBytes, newT.MaxResponseHeaderBytes, "when checking .MaxResponseHeaderBytes") 46 | } 47 | 48 | func TestCleanAuth(t *testing.T) { 49 | for _, test := range []struct { 50 | in string 51 | want string 52 | }{ 53 | {"", ""}, 54 | {"floo", "floo"}, 55 | {"Authorization: ", "Authorization: "}, 56 | {"Authorization: \n", "Authorization: \n"}, 57 | {"Authorization: A", "Authorization: X"}, 58 | {"Authorization: A\n", "Authorization: X\n"}, 59 | {"Authorization: AAAA", "Authorization: XXXX"}, 60 | {"Authorization: AAAA\n", "Authorization: XXXX\n"}, 61 | {"Authorization: AAAAA", "Authorization: XXXX"}, 62 | {"Authorization: AAAAA\n", "Authorization: XXXX\n"}, 63 | {"Authorization: AAAA\n", "Authorization: XXXX\n"}, 64 | {"Authorization: AAAAAAAAA\nPotato: Help\n", "Authorization: XXXX\nPotato: Help\n"}, 65 | {"Sausage: 1\nAuthorization: AAAAAAAAA\nPotato: Help\n", "Sausage: 1\nAuthorization: XXXX\nPotato: Help\n"}, 66 | } { 67 | got := string(cleanAuth([]byte(test.in), Auth[0])) 68 | assert.Equal(t, test.want, got, test.in) 69 | } 70 | } 71 | 72 | func TestCleanAuths(t *testing.T) { 73 | transport := NewDefault(nil) 74 | for _, test := range []struct { 75 | in string 76 | want string 77 | }{ 78 | {"", ""}, 79 | {"floo", "floo"}, 80 | {"Authorization: AAAAAAAAA\nPotato: Help\n", "Authorization: XXXX\nPotato: Help\n"}, 81 | {"X-Auth-Token: AAAAAAAAA\nPotato: Help\n", "X-Auth-Token: XXXX\nPotato: Help\n"}, 82 | {"X-Auth-Token: AAAAAAAAA\nAuthorization: AAAAAAAAA\nPotato: Help\n", "X-Auth-Token: XXXX\nAuthorization: XXXX\nPotato: Help\n"}, 83 | } { 84 | got := string(transport.cleanAuths([]byte(test.in))) 85 | assert.Equal(t, test.want, got, test.in) 86 | } 87 | } 88 | 89 | func TestTransport(t *testing.T) { 90 | const ( 91 | requestBody = "Request text" 92 | responseBody = "Response body" 93 | ) 94 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 95 | fmt.Fprintln(w, responseBody) 96 | if r.Body != nil { 97 | buf, err := ioutil.ReadAll(r.Body) 98 | require.NoError(t, err) 99 | if len(buf) > 0 { 100 | fmt.Fprintln(w, strings.ToUpper(string(buf))) 101 | } 102 | } 103 | })) 104 | defer ts.Close() 105 | 106 | var lines []string 107 | logf := func(format string, v ...interface{}) { 108 | line := fmt.Sprintf(format, v...) 109 | line = strings.Replace(line, "\r", "", -1) 110 | lines = append(lines, line) 111 | } 112 | 113 | for _, test := range []struct { 114 | name string 115 | flags DumpFlags 116 | wantHeaders bool 117 | wantReqBody bool 118 | wantRespBody bool 119 | wantAuth bool 120 | }{ 121 | { 122 | name: "NoDump", 123 | flags: 0, 124 | }, 125 | { 126 | name: "DumpHeaders", 127 | flags: DumpHeaders, 128 | wantHeaders: true, 129 | }, 130 | { 131 | name: "DumpBodies", 132 | flags: DumpBodies, 133 | wantHeaders: true, 134 | wantReqBody: true, 135 | wantRespBody: true, 136 | }, 137 | { 138 | name: "DumpRequests", 139 | flags: DumpRequests, 140 | wantHeaders: true, 141 | wantReqBody: true, 142 | wantRespBody: false, 143 | }, 144 | { 145 | name: "DumpResponses", 146 | flags: DumpResponses, 147 | wantHeaders: true, 148 | wantReqBody: false, 149 | wantRespBody: true, 150 | }, 151 | { 152 | name: "DumpResponsesWithAuth", 153 | flags: DumpResponses | DumpAuth, 154 | wantHeaders: true, 155 | wantReqBody: false, 156 | wantRespBody: true, 157 | wantAuth: true, 158 | }, 159 | } { 160 | t.Run(test.name, func(t *testing.T) { 161 | client := NewClient(&Options{ 162 | Flags: test.flags, 163 | Logf: logf, 164 | }) 165 | lines = nil 166 | 167 | // Do the test request 168 | req, err := http.NewRequest("PUT", ts.URL, bytes.NewBufferString(requestBody)) 169 | require.NoError(t, err) 170 | req.Header.Set("Authorization", "POTATO") 171 | resp, err := client.Do(req) 172 | assert.NoError(t, err) 173 | assert.Equal(t, 200, resp.StatusCode) 174 | body, err := ioutil.ReadAll(resp.Body) 175 | assert.NoError(t, err) 176 | assert.NoError(t, resp.Body.Close()) 177 | expectedResponse := responseBody + "\n" + strings.ToUpper(requestBody) + "\n" 178 | assert.Equal(t, expectedResponse, string(body)) 179 | 180 | if !test.wantHeaders { 181 | assert.Equal(t, 0, len(lines)) 182 | return 183 | } 184 | 185 | // Check what we expect was logged 186 | require.Equal(t, 8, len(lines)) 187 | assert.Equal(t, SeparatorReq, lines[0]) 188 | assert.Contains(t, lines[1], "HTTP REQUEST") 189 | assert.Contains(t, lines[2], "PUT / HTTP") 190 | if test.wantAuth { 191 | assert.Contains(t, lines[2], "\nAuthorization: POTATO\n") 192 | } else { 193 | assert.Contains(t, lines[2], "\nAuthorization: XXXX\n") 194 | } 195 | if test.wantReqBody { 196 | assert.Contains(t, lines[2], requestBody) 197 | } else { 198 | assert.NotContains(t, lines[2], requestBody) 199 | } 200 | assert.Equal(t, SeparatorReq, lines[3]) 201 | assert.Equal(t, SeparatorResp, lines[4]) 202 | assert.Contains(t, lines[5], "HTTP RESPONSE") 203 | assert.Contains(t, lines[6], "200 OK\n") 204 | if test.wantRespBody { 205 | assert.Contains(t, lines[6], expectedResponse) 206 | } else { 207 | assert.NotContains(t, lines[6], expectedResponse) 208 | } 209 | assert.Equal(t, SeparatorResp, lines[7]) 210 | }) 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package debughttp_test 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/rclone/debughttp" 7 | ) 8 | 9 | var ( 10 | existingTransport *http.Transport 11 | transport *debughttp.Transport 12 | client *http.Client 13 | ) 14 | 15 | func myLogf(format string, v ...interface{}) {} 16 | 17 | func ExampleNewClient() { 18 | // Make a client with the defaults which dump headers to log.Printf 19 | client = debughttp.NewClient(nil) 20 | 21 | // Make a client which dumps headers and bodies to log.Printf 22 | client = debughttp.NewClient(&debughttp.DumpBodyOptions) 23 | 24 | // Make a client with full options 25 | // This dumps headers, request bodies and doesn't redact the auth 26 | client = debughttp.NewClient(&debughttp.Options{ 27 | Flags: debughttp.DumpRequests | debughttp.DumpAuth, 28 | Logf: myLogf, 29 | }) 30 | } 31 | 32 | func ExampleNewDefault() { 33 | // Make a transport with the defaults which dump headers to log.Printf 34 | transport = debughttp.NewDefault(nil) 35 | 36 | // Make a transport which dumps headers and bodies to log.Printf 37 | transport = debughttp.NewDefault(&debughttp.DumpBodyOptions) 38 | 39 | // Make a transport with full options 40 | // This dumps headers, request bodies and doesn't redact the auth 41 | transport = debughttp.NewDefault(&debughttp.Options{ 42 | Flags: debughttp.DumpRequests | debughttp.DumpAuth, 43 | Logf: myLogf, 44 | }) 45 | } 46 | 47 | func ExampleNew() { 48 | // Make a transport with the defaults which dump headers to log.Printf 49 | transport = debughttp.New(nil, existingTransport) 50 | 51 | // Make a transport which dumps headers and bodies to log.Printf 52 | transport = debughttp.New(&debughttp.DumpBodyOptions, existingTransport) 53 | 54 | // Make a transport with full options 55 | // This dumps headers, request bodies and doesn't redact the auth 56 | transport = debughttp.New(&debughttp.Options{ 57 | Flags: debughttp.DumpRequests | debughttp.DumpAuth, 58 | Logf: myLogf, 59 | }, existingTransport) 60 | } 61 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rclone/debughttp 2 | 3 | go 1.17 4 | 5 | require github.com/stretchr/testify v1.8.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 12 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | --------------------------------------------------------------------------------