├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── go.yml │ └── lint.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── binary_test.go ├── client_test.go ├── example ├── client │ └── main.go ├── httprepl │ └── main.go └── server │ └── main.go ├── go.mod ├── go.sum ├── httpretty.go ├── httpretty_test.go ├── internal ├── color │ ├── color.go │ └── color_test.go └── header │ ├── header.go │ └── header_test.go ├── printer.go ├── race_test.go ├── recorder.go ├── scripts ├── ci-lint-fmt.sh ├── ci-lint-install.sh ├── ci-lint.sh ├── coverage.sh └── lib.sh ├── server_test.go ├── testdata ├── cert-client.pem ├── cert.pem ├── cert_example.pem ├── certold.pem ├── key-client.pem ├── key.pem ├── key_example.pem ├── keyold.pem ├── log.txtar └── petition.golden └── tls.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: henvic 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every week 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: Code Scanning 2 | # Source: https://github.com/cli/cli/blob/trunk/.github/workflows/codeql.yml 3 | 4 | on: 5 | push: 6 | branches: [ "main" ] 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | # The branches below must be a subset of the branches above 10 | branches: [ "main" ] 11 | schedule: 12 | - cron: '32 13 * * 4' 13 | 14 | jobs: 15 | CodeQL-Build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | steps: 23 | - name: Check out code 24 | uses: actions/checkout@v4 25 | 26 | - name: Initialize CodeQL 27 | uses: github/codeql-action/init@v3 28 | with: 29 | languages: go 30 | 31 | - name: Perform CodeQL Analysis 32 | uses: github/codeql-action/analyze@v3 33 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | types: [opened, synchronize, reopened, ready_for_review] 8 | # The branches below must be a subset of the branches above 9 | branches: [ "main" ] 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | go: [1.23.x, 1.22.x] # when updating versions, update it below too. 17 | runs-on: ${{ matrix.os }} 18 | name: Test 19 | steps: 20 | 21 | - name: Set up Go ${{ matrix.go }} 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go }} 25 | 26 | - name: Check out code 27 | uses: actions/checkout@v4 28 | 29 | - name: Test 30 | # TODO(henvic): Skip generating code coverage when not sending it to Coveralls to speed up testing. 31 | # Remove example directory from code coverage explicitly since after #26 it 32 | # started being considered on the code coverage report and we don't want that. 33 | continue-on-error: ${{ matrix.os != 'ubuntu-latest' || matrix.go != '1.23.x' }} 34 | run: | 35 | go test -race -covermode atomic -coverprofile=profile.cov ./... 36 | sed -i '/^github\.com\/henvic\/httpretty\/example\//d' profile.cov 37 | 38 | - name: Code coverage 39 | if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == '1.23.x' }} 40 | uses: shogo82148/actions-goveralls@v1 41 | with: 42 | path-to-profile: profile.cov 43 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | types: [opened, synchronize, reopened, ready_for_review] 8 | # The branches below must be a subset of the branches above 9 | branches: [ "main" ] 10 | 11 | jobs: 12 | 13 | lint: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | 18 | - name: Set up Go 1.x 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: "1.23.x" 22 | 23 | - name: Check out code 24 | uses: actions/checkout@v4 25 | 26 | - name: Verify dependencies 27 | run: | 28 | go mod verify 29 | go mod download 30 | 31 | - name: Installing static code analysis tools 32 | run: ./scripts/ci-lint-install.sh 33 | 34 | - name: Run checks 35 | run: ./scripts/ci-lint.sh 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Docs 2 | doc/*.md 3 | doc/*.1 4 | 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | 30 | .cover 31 | 32 | coverage.html 33 | coverage.out 34 | 35 | *.coverprofile 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to httpretty 2 | ## Bug reports 3 | When reporting bugs, please add information about your operating system and Go version used to compile the code. 4 | 5 | If you can provide a code snippet reproducing the issue, please do so. 6 | 7 | ## Code 8 | Please write code that satisfies [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) before submitting a pull-request. 9 | Your code should be properly covered by extensive unit tests. 10 | 11 | ## Commit messages 12 | Please follow the Go [commit messages](https://github.com/golang/go/wiki/CommitMessage) convention when contributing code. 13 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Henrique Vicente 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpretty 2 | [](https://pkg.go.dev/github.com/henvic/httpretty) [](https://github.com/henvic/httpretty/actions?query=workflow%3ATests) [](https://coveralls.io/r/henvic/httpretty) [](https://goreportcard.com/report/github.com/henvic/httpretty) [](https://bestpractices.coreinfrastructure.org/projects/3669) 3 | 4 | Package httpretty prints the HTTP requests of your Go programs pretty on your terminal screen. It is mostly inspired in [curl](https://curl.haxx.se)'s `--verbose` mode, and also on the [httputil.DumpRequest](https://golang.org/pkg/net/http/httputil/) and similar functions. 5 | 6 | [](https://asciinema.org/a/297429) 7 | 8 | ## Setting up a logger 9 | You can define a logger with something like 10 | 11 | ```go 12 | logger := &httpretty.Logger{ 13 | Time: true, 14 | TLS: true, 15 | RequestHeader: true, 16 | RequestBody: true, 17 | ResponseHeader: true, 18 | ResponseBody: true, 19 | Colors: true, // erase line if you don't like colors 20 | Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, 21 | } 22 | ``` 23 | 24 | This code will set up a logger with sane settings. By default the logger prints nothing but the request line (and the remote address, when using it on the server-side). 25 | 26 | ### Using on the client-side 27 | You can set the transport for the [*net/http.Client](https://golang.org/pkg/net/http/#Client) you are using like this: 28 | 29 | ```go 30 | client := &http.Client{ 31 | Transport: logger.RoundTripper(http.DefaultTransport), 32 | } 33 | 34 | // from now on, you can use client.Do, client.Get, etc. to create requests. 35 | ``` 36 | 37 | If you don't care about setting a new client, you can safely replace your existing http.DefaultClient with this: 38 | 39 | ```go 40 | http.DefaultClient.Transport = logger.RoundTripper(http.DefaultClient.Transport) 41 | ``` 42 | 43 | Then httpretty is going to print information about regular requests to your terminal when code such as this is called: 44 | ```go 45 | if _, err := http.Get("https://www.google.com/"); err != nil { 46 | fmt.Fprintf(os.Stderr, "%+v\n", err) 47 | os.Exit(1) 48 | } 49 | ``` 50 | 51 | However, have in mind you usually want to use a custom *http.Client to control things such as timeout. 52 | 53 | ## Logging on the server-side 54 | You can use the logger quickly to log requests on your server. For example: 55 | 56 | ```go 57 | logger.Middleware(mux) 58 | ``` 59 | 60 | The handler should by a http.Handler. Usually, you want this to be your `http.ServeMux` HTTP entrypoint. 61 | 62 | For working examples, please see the example directory. 63 | 64 | ## Filtering 65 | You have two ways to filter a request so it isn't printed by the logger. 66 | 67 | ### httpretty.WithHide 68 | You can filter any request by setting a request context before the request reaches `httpretty.RoundTripper`: 69 | 70 | ```go 71 | req = req.WithContext(httpretty.WithHide(ctx)) 72 | ``` 73 | 74 | ### Filter function 75 | A second option is to implement 76 | 77 | ```go 78 | type Filter func(req *http.Request) (skip bool, err error) 79 | ``` 80 | 81 | and set it as the filter for your logger. For example: 82 | 83 | ```go 84 | logger.SetFilter(func filteredURIs(req *http.Request) (bool, error) { 85 | if req.Method != http.MethodGet { 86 | return true, nil 87 | } 88 | 89 | if path := req.URL.Path; path == "/debug" || strings.HasPrefix(path, "/debug/") { 90 | return true, nil 91 | } 92 | 93 | return false 94 | }) 95 | ``` 96 | 97 | ## Formatters 98 | You can define a formatter for any media type by implementing the Formatter interface. 99 | 100 | We provide a JSONFormatter for convenience (it is not enabled by default). 101 | -------------------------------------------------------------------------------- /binary_test.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestIsBinary(t *testing.T) { 9 | testCases := []struct { 10 | desc string 11 | data []byte 12 | binary bool 13 | }{ 14 | { 15 | desc: "Empty", 16 | binary: false, 17 | }, 18 | { 19 | desc: "Text", 20 | data: []byte("plain text"), 21 | binary: false, 22 | }, 23 | { 24 | desc: "More text", 25 | data: []byte("plain text\n"), 26 | binary: false, 27 | }, 28 | { 29 | desc: "Text with UTF16 Big Endian BOM", 30 | data: []byte("\xFE\xFFevil plain text"), 31 | binary: false, 32 | }, 33 | { 34 | desc: "Text with UTF16 Little Endian BOM", 35 | data: []byte("\xFF\xFEevil plain text"), 36 | binary: false, 37 | }, 38 | { 39 | desc: "Text with UTF8 BOM", 40 | data: []byte("\xEF\xBB\xBFevil plain text"), 41 | binary: false, 42 | }, 43 | { 44 | desc: "Binary", 45 | data: []byte{1, 2, 3}, 46 | binary: true, 47 | }, 48 | { 49 | desc: "Binary over 512bytes", 50 | data: bytes.Repeat([]byte{1, 2, 3, 4, 5, 6, 7, 8}, 65), 51 | binary: true, 52 | }, 53 | { 54 | desc: "JPEG image", 55 | data: []byte("\xFF\xD8\xFF"), 56 | binary: true, 57 | }, 58 | { 59 | desc: "AVI video", 60 | data: []byte("RIFF,O\n\x00AVI LISTÀ"), 61 | binary: true, 62 | }, 63 | { 64 | desc: "RAR", 65 | data: []byte("Rar!\x1A\x07\x00"), 66 | binary: true, 67 | }, 68 | { 69 | desc: "PDF", 70 | data: []byte("\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7"), 71 | binary: true, 72 | }, 73 | } 74 | for _, tc := range testCases { 75 | t.Run(tc.desc, func(t *testing.T) { 76 | if got := isBinary(tc.data); got != tc.binary { 77 | t.Errorf("wanted isBinary(%v) = %v, got %v instead", tc.data, tc.binary, got) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | _ "embed" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "log" 14 | "mime" 15 | "mime/multipart" 16 | "net" 17 | "net/http" 18 | "net/http/httptest" 19 | "net/http/httputil" 20 | "net/url" 21 | "os" 22 | "regexp" 23 | "strings" 24 | "sync" 25 | "testing" 26 | "time" 27 | 28 | "golang.org/x/tools/txtar" 29 | ) 30 | 31 | type helloHandler struct{} 32 | 33 | func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 34 | w.Header()["Date"] = nil 35 | fmt.Fprintf(w, "Hello, world!") 36 | } 37 | 38 | var ( 39 | //go:embed testdata/log.txtar 40 | dump []byte 41 | dumpA = txtar.Parse(dump) 42 | ) 43 | 44 | func golden(name string) string { 45 | for _, f := range dumpA.Files { 46 | if name == f.Name { 47 | return string(f.Data) 48 | } 49 | } 50 | panic("golden file not found") 51 | } 52 | 53 | func TestOutgoing(t *testing.T) { 54 | // important: cannot be in parallel because we are capturing os.Stdout 55 | ts := httptest.NewServer(&helloHandler{}) 56 | defer ts.Close() 57 | logger := &Logger{ 58 | TLS: true, 59 | RequestHeader: true, 60 | RequestBody: true, 61 | ResponseHeader: true, 62 | ResponseBody: true, 63 | } 64 | client := &http.Client{ 65 | // Only use the default transport (http.DefaultTransport) on TestOutgoing. 66 | // Passing nil here = http.DefaultTransport. 67 | Transport: logger.RoundTripper(nil), 68 | } 69 | // code for capturing stdout was copied from the Go source code file src/testing/run_example.go: 70 | // https://github.com/golang/go/blob/ac56baa/src/testing/run_example.go 71 | stdout := os.Stdout 72 | r, w, err := os.Pipe() 73 | if err != nil { 74 | panic(err) 75 | } 76 | os.Stdout = w 77 | outC := make(chan string) 78 | go func() { 79 | var buf strings.Builder 80 | _, errcp := io.Copy(&buf, r) 81 | r.Close() 82 | if errcp != nil { 83 | panic(errcp) 84 | } 85 | outC <- buf.String() 86 | }() 87 | var want string 88 | defer func() { 89 | w.Close() 90 | os.Stdout = stdout 91 | if out := <-outC; out != want { 92 | t.Errorf("logged HTTP request %s; want %s", out, want) 93 | } 94 | }() 95 | 96 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 97 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 98 | if err != nil { 99 | t.Errorf("cannot create request: %v", err) 100 | } 101 | resp, err := client.Do(req) 102 | if err != nil { 103 | t.Errorf("cannot connect to the server: %v", err) 104 | } 105 | defer resp.Body.Close() 106 | 107 | // see preceding deferred function, where want is used. 108 | want = fmt.Sprintf(golden(t.Name()), ts.URL, ts.Listener.Addr()) 109 | testBody(t, resp.Body, []byte("Hello, world!")) 110 | } 111 | 112 | func outgoingGet(t *testing.T, client *http.Client, ts *httptest.Server, done func()) { 113 | defer done() 114 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 115 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 116 | if err != nil { 117 | t.Errorf("cannot create request: %v", err) 118 | } 119 | resp, err := client.Do(req) 120 | if err != nil { 121 | t.Errorf("cannot connect to the server: %v", err) 122 | } 123 | defer resp.Body.Close() 124 | testBody(t, resp.Body, []byte("Hello, world!")) 125 | } 126 | 127 | func TestOutgoingConcurrency(t *testing.T) { 128 | // don't run in parallel 129 | if race { 130 | t.Skip("cannot test because until data race issues are resolved on the standard library https://github.com/golang/go/issues/30597") 131 | } 132 | 133 | ts := httptest.NewServer(&helloHandler{}) 134 | defer ts.Close() 135 | logger := &Logger{ 136 | TLS: true, 137 | RequestHeader: true, 138 | RequestBody: true, 139 | ResponseHeader: true, 140 | ResponseBody: true, 141 | } 142 | logger.SetFlusher(OnEnd) 143 | var buf bytes.Buffer 144 | logger.SetOutput(&buf) 145 | client := &http.Client{ 146 | Transport: logger.RoundTripper(newTransport()), 147 | } 148 | 149 | var wg sync.WaitGroup 150 | concurrency := 100 151 | wg.Add(concurrency) 152 | for i := 0; i < concurrency; i++ { 153 | go outgoingGet(t, client, ts, wg.Done) 154 | time.Sleep(time.Millisecond) // let's slow down just a little bit ("too many files descriptors open" on a slow machine, more realistic traffic, and so on) 155 | } 156 | wg.Wait() 157 | got := buf.String() 158 | if gotConcurrency := strings.Count(got, "< HTTP/1.1 200 OK"); concurrency != gotConcurrency { 159 | t.Errorf("logged %d requests, wanted %d", concurrency, gotConcurrency) 160 | } 161 | if want := fmt.Sprintf(golden(t.Name()), ts.URL, ts.Listener.Addr()); !strings.Contains(got, want) { 162 | t.Errorf("Request doesn't contain expected body") 163 | } 164 | } 165 | 166 | func TestOutgoingMinimal(t *testing.T) { 167 | t.Parallel() 168 | ts := httptest.NewServer(&helloHandler{}) 169 | defer ts.Close() 170 | 171 | // only prints the request URI. 172 | logger := &Logger{} 173 | var buf bytes.Buffer 174 | logger.SetOutput(&buf) 175 | client := &http.Client{ 176 | Transport: logger.RoundTripper(newTransport()), 177 | } 178 | 179 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 180 | if err != nil { 181 | t.Errorf("cannot create request: %v", err) 182 | } 183 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 184 | req.AddCookie(&http.Cookie{ 185 | Name: "food", 186 | Value: "sorbet", 187 | }) 188 | if _, err = client.Do(req); err != nil { 189 | t.Errorf("cannot connect to the server: %v", err) 190 | } 191 | if want, got := fmt.Sprintf("* Request to %s\n", ts.URL), buf.String(); got != want { 192 | t.Errorf("logged HTTP request %s; want %s", got, want) 193 | } 194 | } 195 | 196 | func TestOutgoingSanitized(t *testing.T) { 197 | t.Parallel() 198 | ts := httptest.NewServer(&helloHandler{}) 199 | defer ts.Close() 200 | 201 | logger := &Logger{ 202 | RequestHeader: true, 203 | RequestBody: true, 204 | ResponseHeader: true, 205 | ResponseBody: true, 206 | } 207 | var buf bytes.Buffer 208 | logger.SetOutput(&buf) 209 | client := &http.Client{ 210 | Transport: logger.RoundTripper(newTransport()), 211 | } 212 | 213 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 214 | if err != nil { 215 | t.Errorf("cannot create request: %v", err) 216 | } 217 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 218 | req.AddCookie(&http.Cookie{ 219 | Name: "food", 220 | Value: "sorbet", 221 | }) 222 | if _, err = client.Do(req); err != nil { 223 | t.Errorf("cannot connect to the server: %v", err) 224 | } 225 | want := fmt.Sprintf(golden(t.Name()), ts.URL, ts.Listener.Addr()) 226 | if got := buf.String(); got != want { 227 | t.Errorf("logged HTTP request %s; want %s", got, want) 228 | } 229 | } 230 | 231 | func TestOutgoingSkipSanitize(t *testing.T) { 232 | t.Parallel() 233 | ts := httptest.NewServer(&helloHandler{}) 234 | defer ts.Close() 235 | 236 | logger := &Logger{ 237 | RequestHeader: true, 238 | RequestBody: true, 239 | ResponseHeader: true, 240 | ResponseBody: true, 241 | SkipSanitize: true, 242 | } 243 | var buf bytes.Buffer 244 | logger.SetOutput(&buf) 245 | client := &http.Client{ 246 | Transport: logger.RoundTripper(newTransport()), 247 | } 248 | 249 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 250 | if err != nil { 251 | t.Errorf("cannot create request: %v", err) 252 | } 253 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 254 | req.AddCookie(&http.Cookie{ 255 | Name: "food", 256 | Value: "sorbet", 257 | }) 258 | if _, err = client.Do(req); err != nil { 259 | t.Errorf("cannot connect to the server: %v", err) 260 | } 261 | want := fmt.Sprintf(golden(t.Name()), ts.URL, ts.Listener.Addr()) 262 | if got := buf.String(); got != want { 263 | t.Errorf("logged HTTP request %s; want %s", got, want) 264 | } 265 | } 266 | 267 | func TestOutgoingHide(t *testing.T) { 268 | t.Parallel() 269 | ts := httptest.NewServer(&helloHandler{}) 270 | defer ts.Close() 271 | 272 | logger := &Logger{ 273 | RequestHeader: true, 274 | RequestBody: true, 275 | ResponseHeader: true, 276 | ResponseBody: true, 277 | } 278 | var buf bytes.Buffer 279 | logger.SetOutput(&buf) 280 | client := &http.Client{ 281 | Transport: logger.RoundTripper(newTransport()), 282 | } 283 | 284 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 285 | if err != nil { 286 | t.Errorf("cannot create request: %v", err) 287 | } 288 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 289 | req = req.WithContext(WithHide(context.Background())) 290 | if _, err = client.Do(req); err != nil { 291 | t.Errorf("cannot connect to the server: %v", err) 292 | } 293 | if buf.Len() != 0 { 294 | t.Errorf("request should not be logged, got %v", buf.String()) 295 | } 296 | if got := buf.String(); got != "" { 297 | t.Errorf("logged HTTP request %s; want none", got) 298 | } 299 | } 300 | 301 | func filteredURIs(req *http.Request) (bool, error) { 302 | path := req.URL.Path 303 | if path == "/filtered" { 304 | return true, nil 305 | } 306 | if path == "/unfiltered" { 307 | return false, nil 308 | } 309 | return false, errors.New("filter error triggered") 310 | } 311 | 312 | func TestOutgoingFilter(t *testing.T) { 313 | t.Parallel() 314 | ts := httptest.NewServer(&helloHandler{}) 315 | defer ts.Close() 316 | 317 | logger := &Logger{ 318 | RequestHeader: true, 319 | RequestBody: true, 320 | ResponseHeader: true, 321 | ResponseBody: true, 322 | } 323 | logger.SetOutput(io.Discard) 324 | logger.SetFilter(filteredURIs) 325 | client := &http.Client{ 326 | Transport: logger.RoundTripper(newTransport()), 327 | } 328 | 329 | testCases := []struct { 330 | uri string 331 | want string 332 | }{ 333 | {uri: "filtered"}, 334 | {uri: "unfiltered", want: "* Request"}, 335 | {uri: "other", want: "filter error triggered"}, 336 | } 337 | for _, tc := range testCases { 338 | t.Run(tc.uri, func(t *testing.T) { 339 | var buf bytes.Buffer 340 | logger.SetOutput(&buf) 341 | if _, err := client.Get(fmt.Sprintf("%s/%s", ts.URL, tc.uri)); err != nil { 342 | t.Errorf("cannot create request: %v", err) 343 | } 344 | if tc.want == "" && buf.Len() != 0 { 345 | t.Errorf("wanted input to be filtered, got %v instead", buf.String()) 346 | } 347 | if !strings.Contains(buf.String(), tc.want) { 348 | t.Errorf(`expected input to contain "%v", got %v instead`, tc.want, buf.String()) 349 | } 350 | }) 351 | } 352 | } 353 | 354 | func TestOutgoingFilterPanicked(t *testing.T) { 355 | t.Parallel() 356 | ts := httptest.NewServer(&helloHandler{}) 357 | defer ts.Close() 358 | 359 | logger := &Logger{ 360 | RequestHeader: true, 361 | RequestBody: true, 362 | ResponseHeader: true, 363 | ResponseBody: true, 364 | } 365 | logger.SetOutput(io.Discard) 366 | logger.SetFilter(func(req *http.Request) (bool, error) { 367 | panic("evil panic") 368 | }) 369 | client := &http.Client{ 370 | Transport: logger.RoundTripper(newTransport()), 371 | } 372 | 373 | var buf bytes.Buffer 374 | logger.SetOutput(&buf) 375 | if _, err := client.Get(ts.URL); err != nil { 376 | t.Errorf("cannot create request: %v", err) 377 | } 378 | want := fmt.Sprintf(golden(t.Name()), ts.URL, ts.URL, ts.Listener.Addr()) 379 | if got := buf.String(); got != want { 380 | t.Errorf(`expected input to contain "%v", got %v instead`, want, got) 381 | } 382 | } 383 | 384 | func TestOutgoingSkipHeader(t *testing.T) { 385 | t.Parallel() 386 | ts := httptest.NewServer(&jsonHandler{}) 387 | defer ts.Close() 388 | 389 | logger := Logger{ 390 | RequestHeader: true, 391 | RequestBody: true, 392 | ResponseHeader: true, 393 | ResponseBody: true, 394 | } 395 | logger.SkipHeader([]string{ 396 | "user-agent", 397 | "content-type", 398 | }) 399 | var buf bytes.Buffer 400 | logger.SetOutput(&buf) 401 | client := &http.Client{ 402 | Transport: logger.RoundTripper(newTransport()), 403 | } 404 | 405 | uri := fmt.Sprintf("%s/json", ts.URL) 406 | req, err := http.NewRequest(http.MethodGet, uri, nil) 407 | if err != nil { 408 | t.Errorf("cannot create request: %v", err) 409 | } 410 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 411 | if _, err = client.Do(req); err != nil { 412 | t.Errorf("cannot connect to the server: %v", err) 413 | } 414 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 415 | if got := buf.String(); got != want { 416 | t.Errorf("logged HTTP request %s; want %s", got, want) 417 | } 418 | } 419 | 420 | func TestOutgoingBodyFilter(t *testing.T) { 421 | t.Parallel() 422 | ts := httptest.NewServer(&jsonHandler{}) 423 | defer ts.Close() 424 | 425 | logger := Logger{ 426 | RequestHeader: true, 427 | RequestBody: true, 428 | ResponseHeader: true, 429 | ResponseBody: true, 430 | } 431 | logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { 432 | mediatype, _, _ := mime.ParseMediaType(h.Get("Content-Type")) 433 | return mediatype == "application/json", nil 434 | }) 435 | var buf bytes.Buffer 436 | logger.SetOutput(&buf) 437 | client := &http.Client{ 438 | Transport: logger.RoundTripper(newTransport()), 439 | } 440 | 441 | uri := fmt.Sprintf("%s/json", ts.URL) 442 | req, err := http.NewRequest(http.MethodGet, uri, nil) 443 | if err != nil { 444 | t.Errorf("cannot create request: %v", err) 445 | } 446 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 447 | if _, err = client.Do(req); err != nil { 448 | t.Errorf("cannot connect to the server: %v", err) 449 | } 450 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 451 | if got := buf.String(); got != want { 452 | t.Errorf("logged HTTP request %s; want %s", got, want) 453 | } 454 | } 455 | 456 | func TestOutgoingBodyFilterSoftError(t *testing.T) { 457 | t.Parallel() 458 | ts := httptest.NewServer(&jsonHandler{}) 459 | defer ts.Close() 460 | 461 | logger := Logger{ 462 | RequestHeader: true, 463 | RequestBody: true, 464 | ResponseHeader: true, 465 | ResponseBody: true, 466 | } 467 | logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { 468 | // filter anyway, but print soft error saying something went wrong during the filtering. 469 | return true, errors.New("incomplete implementation") 470 | }) 471 | var buf bytes.Buffer 472 | logger.SetOutput(&buf) 473 | client := &http.Client{ 474 | Transport: logger.RoundTripper(newTransport()), 475 | } 476 | 477 | uri := fmt.Sprintf("%s/json", ts.URL) 478 | req, err := http.NewRequest(http.MethodGet, uri, nil) 479 | if err != nil { 480 | t.Errorf("cannot create request: %v", err) 481 | } 482 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 483 | if _, err = client.Do(req); err != nil { 484 | t.Errorf("cannot connect to the server: %v", err) 485 | } 486 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 487 | if got := buf.String(); got != want { 488 | t.Errorf("logged HTTP request %s; want %s", got, want) 489 | } 490 | } 491 | 492 | func TestOutgoingBodyFilterPanicked(t *testing.T) { 493 | t.Parallel() 494 | ts := httptest.NewServer(&jsonHandler{}) 495 | defer ts.Close() 496 | 497 | logger := Logger{ 498 | RequestHeader: true, 499 | RequestBody: true, 500 | ResponseHeader: true, 501 | ResponseBody: true, 502 | } 503 | logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { 504 | panic("evil panic") 505 | }) 506 | var buf bytes.Buffer 507 | logger.SetOutput(&buf) 508 | client := &http.Client{ 509 | Transport: logger.RoundTripper(newTransport()), 510 | } 511 | 512 | uri := fmt.Sprintf("%s/json", ts.URL) 513 | req, err := http.NewRequest(http.MethodGet, uri, nil) 514 | if err != nil { 515 | t.Errorf("cannot create request: %v", err) 516 | } 517 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 518 | if _, err = client.Do(req); err != nil { 519 | t.Errorf("cannot connect to the server: %v", err) 520 | } 521 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 522 | if got := buf.String(); got != want { 523 | t.Errorf("logged HTTP request %s; want %s", got, want) 524 | } 525 | } 526 | 527 | func TestOutgoingWithTimeRequest(t *testing.T) { 528 | t.Parallel() 529 | ts := httptest.NewServer(&helloHandler{}) 530 | defer ts.Close() 531 | 532 | logger := &Logger{ 533 | Time: true, 534 | 535 | RequestHeader: true, 536 | RequestBody: true, 537 | ResponseHeader: true, 538 | ResponseBody: true, 539 | } 540 | var buf bytes.Buffer 541 | logger.SetOutput(&buf) 542 | client := &http.Client{ 543 | Transport: logger.RoundTripper(newTransport()), 544 | } 545 | 546 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 547 | if err != nil { 548 | t.Errorf("cannot create request: %v", err) 549 | } 550 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 551 | if _, err = client.Do(req); err != nil { 552 | t.Errorf("cannot connect to the server: %v", err) 553 | } 554 | got := buf.String() 555 | if !strings.Contains(got, "* Request at ") { 556 | t.Error("missing printing start time of request") 557 | } 558 | if !strings.Contains(got, "* Request took ") { 559 | t.Error("missing printing request duration") 560 | } 561 | } 562 | 563 | type jsonHandler struct{} 564 | 565 | func (h jsonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 566 | w.Header()["Date"] = nil 567 | if r.URL.Path == "/vnd" { 568 | w.Header().Set("Content-Type", "application/vnd.api+json") 569 | } else { 570 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 571 | } 572 | type res struct { 573 | Result string `json:"result"` 574 | Number json.Number `json:"number"` 575 | } 576 | b, err := json.Marshal(res{ 577 | Result: "Hello, world!", 578 | Number: json.Number("3.14"), 579 | }) 580 | if err != nil { 581 | http.Error(w, err.Error(), http.StatusInternalServerError) 582 | return 583 | } 584 | fmt.Fprint(w, string(b)) 585 | } 586 | 587 | func TestOutgoingFormattedJSON(t *testing.T) { 588 | t.Parallel() 589 | ts := httptest.NewServer(&jsonHandler{}) 590 | defer ts.Close() 591 | 592 | logger := Logger{ 593 | RequestHeader: true, 594 | RequestBody: true, 595 | ResponseHeader: true, 596 | ResponseBody: true, 597 | Formatters: []Formatter{ 598 | &JSONFormatter{}, 599 | }, 600 | } 601 | var buf bytes.Buffer 602 | logger.SetOutput(&buf) 603 | client := &http.Client{ 604 | Transport: logger.RoundTripper(newTransport()), 605 | } 606 | 607 | testCases := []struct { 608 | name string 609 | contentType string 610 | }{ 611 | { 612 | name: "json", 613 | contentType: "application/json", 614 | }, 615 | { 616 | name: "vnd", 617 | contentType: "application/vnd.api+json", 618 | }, 619 | } 620 | for _, tc := range testCases { 621 | t.Run(tc.name, func(t *testing.T) { 622 | buf.Reset() 623 | uri := fmt.Sprintf("%s/%s", ts.URL, tc.name) 624 | req, err := http.NewRequest(http.MethodGet, uri, nil) 625 | if err != nil { 626 | t.Errorf("cannot create request: %v", err) 627 | } 628 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 629 | if _, err = client.Do(req); err != nil { 630 | t.Errorf("cannot connect to the server: %v", err) 631 | } 632 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 633 | if got := buf.String(); got != want { 634 | t.Errorf("logged HTTP request %s; want %s", got, want) 635 | } 636 | }) 637 | } 638 | } 639 | 640 | type badJSONHandler struct{} 641 | 642 | func (h badJSONHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 643 | w.Header()["Date"] = nil 644 | w.Header().Set("Content-Type", "application/json; charset=utf-8") // wrong content-type on purpose 645 | fmt.Fprint(w, `{"bad": }`) 646 | } 647 | 648 | func TestOutgoingBadJSON(t *testing.T) { 649 | t.Parallel() 650 | ts := httptest.NewServer(&badJSONHandler{}) 651 | defer ts.Close() 652 | 653 | logger := &Logger{ 654 | RequestHeader: true, 655 | RequestBody: true, 656 | ResponseHeader: true, 657 | ResponseBody: true, 658 | Formatters: []Formatter{ 659 | &JSONFormatter{}, 660 | }, 661 | } 662 | var buf bytes.Buffer 663 | logger.SetOutput(&buf) 664 | client := &http.Client{ 665 | Transport: logger.RoundTripper(newTransport()), 666 | } 667 | 668 | uri := fmt.Sprintf("%s/json", ts.URL) 669 | req, err := http.NewRequest(http.MethodGet, uri, nil) 670 | if err != nil { 671 | t.Errorf("cannot create request: %v", err) 672 | } 673 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 674 | if _, err = client.Do(req); err != nil { 675 | t.Errorf("cannot connect to the server: %v", err) 676 | } 677 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 678 | if got := buf.String(); got != want { 679 | t.Errorf("logged HTTP request %s; want %s", got, want) 680 | } 681 | } 682 | 683 | type panickingFormatter struct{} 684 | 685 | func (p *panickingFormatter) Match(mediatype string) bool { 686 | return true 687 | } 688 | 689 | func (p *panickingFormatter) Format(w io.Writer, src []byte) error { 690 | panic("evil formatter") 691 | } 692 | 693 | func TestOutgoingFormatterPanicked(t *testing.T) { 694 | t.Parallel() 695 | ts := httptest.NewServer(&badJSONHandler{}) 696 | defer ts.Close() 697 | 698 | logger := &Logger{ 699 | RequestHeader: true, 700 | RequestBody: true, 701 | ResponseHeader: true, 702 | ResponseBody: true, 703 | Formatters: []Formatter{ 704 | &panickingFormatter{}, 705 | }, 706 | } 707 | var buf bytes.Buffer 708 | logger.SetOutput(&buf) 709 | client := &http.Client{ 710 | Transport: logger.RoundTripper(newTransport()), 711 | } 712 | 713 | uri := fmt.Sprintf("%s/json", ts.URL) 714 | req, err := http.NewRequest(http.MethodGet, uri, nil) 715 | if err != nil { 716 | t.Errorf("cannot create request: %v", err) 717 | } 718 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 719 | if _, err = client.Do(req); err != nil { 720 | t.Errorf("cannot connect to the server: %v", err) 721 | } 722 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 723 | if got := buf.String(); got != want { 724 | t.Errorf("logged HTTP request %s; want %s", got, want) 725 | } 726 | } 727 | 728 | type panickingFormatterMatcher struct{} 729 | 730 | func (p *panickingFormatterMatcher) Match(mediatype string) bool { 731 | panic("evil matcher") 732 | } 733 | 734 | func (p *panickingFormatterMatcher) Format(w io.Writer, src []byte) error { 735 | return nil 736 | } 737 | 738 | func TestOutgoingFormatterMatcherPanicked(t *testing.T) { 739 | t.Parallel() 740 | ts := httptest.NewServer(&badJSONHandler{}) 741 | defer ts.Close() 742 | 743 | logger := &Logger{ 744 | RequestHeader: true, 745 | RequestBody: true, 746 | ResponseHeader: true, 747 | ResponseBody: true, 748 | Formatters: []Formatter{ 749 | &panickingFormatterMatcher{}, 750 | }, 751 | } 752 | var buf bytes.Buffer 753 | logger.SetOutput(&buf) 754 | client := &http.Client{ 755 | Transport: logger.RoundTripper(newTransport()), 756 | } 757 | 758 | uri := fmt.Sprintf("%s/json", ts.URL) 759 | req, err := http.NewRequest(http.MethodGet, uri, nil) 760 | if err != nil { 761 | t.Errorf("cannot create request: %v", err) 762 | } 763 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 764 | if _, err = client.Do(req); err != nil { 765 | t.Errorf("cannot connect to the server: %v", err) 766 | } 767 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 768 | if got := buf.String(); got != want { 769 | t.Errorf("logged HTTP request %s; want %s", got, want) 770 | } 771 | } 772 | 773 | type formHandler struct{} 774 | 775 | func (h formHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 776 | w.Header()["Date"] = nil 777 | fmt.Fprint(w, "form received") 778 | } 779 | 780 | func TestOutgoingForm(t *testing.T) { 781 | t.Parallel() 782 | ts := httptest.NewServer(&formHandler{}) 783 | defer ts.Close() 784 | 785 | logger := &Logger{ 786 | RequestHeader: true, 787 | RequestBody: true, 788 | ResponseHeader: true, 789 | ResponseBody: true, 790 | Formatters: []Formatter{ 791 | &JSONFormatter{}, 792 | }, 793 | } 794 | var buf bytes.Buffer 795 | logger.SetOutput(&buf) 796 | client := &http.Client{ 797 | Transport: logger.RoundTripper(newTransport()), 798 | } 799 | 800 | form := url.Values{} 801 | form.Add("foo", "bar") 802 | form.Add("email", "root@example.com") 803 | uri := fmt.Sprintf("%s/form", ts.URL) 804 | req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(form.Encode())) 805 | if err != nil { 806 | t.Errorf("cannot create request: %v", err) 807 | } 808 | if _, err = client.Do(req); err != nil { 809 | t.Errorf("cannot connect to the server: %v", err) 810 | } 811 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 812 | if got := buf.String(); got != want { 813 | t.Errorf("logged HTTP request %s; want %s", got, want) 814 | } 815 | } 816 | 817 | func TestOutgoingBinaryBody(t *testing.T) { 818 | t.Parallel() 819 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 820 | w.Header()["Date"] = nil 821 | fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") 822 | })) 823 | defer ts.Close() 824 | 825 | logger := &Logger{ 826 | RequestHeader: true, 827 | RequestBody: true, 828 | ResponseHeader: true, 829 | ResponseBody: true, 830 | } 831 | var buf bytes.Buffer 832 | logger.SetOutput(&buf) 833 | client := &http.Client{ 834 | Transport: logger.RoundTripper(newTransport()), 835 | } 836 | 837 | b := []byte("RIFF\x00\x00\x00\x00WEBPVP") 838 | uri := fmt.Sprintf("%s/convert", ts.URL) 839 | req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) 840 | if err != nil { 841 | t.Errorf("cannot create request: %v", err) 842 | } 843 | req.Header.Add("Content-Type", "image/webp") 844 | if _, err = client.Do(req); err != nil { 845 | t.Errorf("cannot connect to the server: %v", err) 846 | } 847 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 848 | if got := buf.String(); got != want { 849 | t.Errorf("logged HTTP request %s; want %s", got, want) 850 | } 851 | } 852 | 853 | func TestOutgoingBinaryBodyNoMediatypeHeader(t *testing.T) { 854 | t.Parallel() 855 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 856 | w.Header()["Date"] = nil 857 | w.Header()["Content-Type"] = nil 858 | fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") 859 | })) 860 | defer ts.Close() 861 | 862 | logger := &Logger{ 863 | RequestHeader: true, 864 | RequestBody: true, 865 | ResponseHeader: true, 866 | ResponseBody: true, 867 | } 868 | var buf bytes.Buffer 869 | logger.SetOutput(&buf) 870 | client := &http.Client{ 871 | Transport: logger.RoundTripper(newTransport()), 872 | } 873 | 874 | b := []byte("RIFF\x00\x00\x00\x00WEBPVP") 875 | uri := fmt.Sprintf("%s/convert", ts.URL) 876 | req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) 877 | if err != nil { 878 | t.Errorf("cannot create request: %v", err) 879 | } 880 | 881 | if _, err = client.Do(req); err != nil { 882 | t.Errorf("cannot connect to the server: %v", err) 883 | } 884 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 885 | if got := buf.String(); got != want { 886 | t.Errorf("logged HTTP request %s; want %s", got, want) 887 | } 888 | } 889 | 890 | type longRequestHandler struct{} 891 | 892 | func (h longRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 893 | w.Header()["Date"] = nil 894 | fmt.Fprint(w, "long request received") 895 | } 896 | 897 | func TestOutgoingLongRequest(t *testing.T) { 898 | t.Parallel() 899 | ts := httptest.NewServer(&longRequestHandler{}) 900 | defer ts.Close() 901 | 902 | logger := &Logger{ 903 | RequestHeader: true, 904 | RequestBody: true, 905 | ResponseHeader: true, 906 | ResponseBody: true, 907 | Formatters: []Formatter{ 908 | &JSONFormatter{}, 909 | }, 910 | } 911 | var buf bytes.Buffer 912 | logger.SetOutput(&buf) 913 | client := &http.Client{ 914 | Transport: logger.RoundTripper(newTransport()), 915 | } 916 | 917 | uri := fmt.Sprintf("%s/long-request", ts.URL) 918 | req, err := http.NewRequest(http.MethodPut, uri, strings.NewReader(petition)) 919 | if err != nil { 920 | t.Errorf("cannot create request: %v", err) 921 | } 922 | if _, err = client.Do(req); err != nil { 923 | t.Errorf("cannot connect to the server: %v", err) 924 | } 925 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr(), petition) 926 | if got := buf.String(); got != want { 927 | t.Errorf("logged HTTP request %s; want %s", got, want) 928 | } 929 | } 930 | 931 | type longResponseHandler struct{} 932 | 933 | func (h longResponseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 934 | w.Header()["Date"] = nil 935 | w.Header().Set("Content-Length", fmt.Sprintf("%d", len(petition))) 936 | if r.Method != http.MethodHead { 937 | fmt.Fprint(w, petition) 938 | } 939 | } 940 | 941 | func TestOutgoingLongResponse(t *testing.T) { 942 | t.Parallel() 943 | ts := httptest.NewServer(&longResponseHandler{}) 944 | defer ts.Close() 945 | 946 | logger := &Logger{ 947 | RequestHeader: true, 948 | RequestBody: true, 949 | ResponseHeader: true, 950 | ResponseBody: true, 951 | MaxResponseBody: int64(len(petition) + 1000), // value larger than the text 952 | } 953 | var buf bytes.Buffer 954 | logger.SetOutput(&buf) 955 | 956 | client := &http.Client{ 957 | Transport: logger.RoundTripper(newTransport()), 958 | } 959 | uri := fmt.Sprintf("%s/long-response", ts.URL) 960 | req, err := http.NewRequest(http.MethodGet, uri, nil) 961 | if err != nil { 962 | t.Errorf("cannot create request: %v", err) 963 | } 964 | resp, err := client.Do(req) 965 | if err != nil { 966 | t.Errorf("cannot connect to the server: %v", err) 967 | } 968 | defer resp.Body.Close() 969 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr(), petition) 970 | if got := buf.String(); got != want { 971 | t.Errorf("logged HTTP request %s; want %s", got, want) 972 | } 973 | testBody(t, resp.Body, []byte(petition)) 974 | } 975 | 976 | func TestOutgoingLongResponseHead(t *testing.T) { 977 | t.Parallel() 978 | ts := httptest.NewServer(&longResponseHandler{}) 979 | defer ts.Close() 980 | logger := &Logger{ 981 | RequestHeader: true, 982 | RequestBody: true, 983 | ResponseHeader: true, 984 | ResponseBody: true, 985 | MaxResponseBody: int64(len(petition) + 1000), // value larger than the text 986 | } 987 | var buf bytes.Buffer 988 | logger.SetOutput(&buf) 989 | client := &http.Client{ 990 | Transport: logger.RoundTripper(newTransport()), 991 | } 992 | 993 | uri := fmt.Sprintf("%s/long-response", ts.URL) 994 | req, err := http.NewRequest(http.MethodHead, uri, nil) 995 | if err != nil { 996 | t.Errorf("cannot create request: %v", err) 997 | } 998 | resp, err := client.Do(req) 999 | if err != nil { 1000 | t.Errorf("cannot connect to the server: %v", err) 1001 | } 1002 | defer resp.Body.Close() 1003 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 1004 | if got := buf.String(); got != want { 1005 | t.Errorf("logged HTTP request %s; want %s", got, want) 1006 | } 1007 | testBody(t, resp.Body, []byte{}) 1008 | } 1009 | 1010 | func TestOutgoingTooLongResponse(t *testing.T) { 1011 | t.Parallel() 1012 | ts := httptest.NewServer(&longResponseHandler{}) 1013 | defer ts.Close() 1014 | logger := &Logger{ 1015 | RequestHeader: true, 1016 | RequestBody: true, 1017 | ResponseHeader: true, 1018 | ResponseBody: true, 1019 | MaxResponseBody: 5000, // value smaller than the text 1020 | } 1021 | var buf bytes.Buffer 1022 | logger.SetOutput(&buf) 1023 | client := &http.Client{ 1024 | Transport: logger.RoundTripper(newTransport()), 1025 | } 1026 | 1027 | uri := fmt.Sprintf("%s/long-response", ts.URL) 1028 | req, err := http.NewRequest(http.MethodGet, uri, nil) 1029 | if err != nil { 1030 | t.Errorf("cannot create request: %v", err) 1031 | } 1032 | resp, err := client.Do(req) 1033 | if err != nil { 1034 | t.Errorf("cannot connect to the server: %v", err) 1035 | } 1036 | defer resp.Body.Close() 1037 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr()) 1038 | if got := buf.String(); got != want { 1039 | t.Errorf("logged HTTP request %s; want %s", got, want) 1040 | } 1041 | testBody(t, resp.Body, []byte(petition)) 1042 | } 1043 | 1044 | type longResponseUnknownLengthHandler struct { 1045 | repeat int 1046 | } 1047 | 1048 | func (h longResponseUnknownLengthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1049 | w.Header()["Date"] = nil 1050 | fmt.Fprint(w, strings.Repeat(petition, h.repeat+1)) 1051 | } 1052 | 1053 | func TestOutgoingLongResponseUnknownLength(t *testing.T) { 1054 | t.Parallel() 1055 | testCases := []struct { 1056 | name string 1057 | repeat int 1058 | }{ 1059 | {name: "short", repeat: 1}, 1060 | {name: "long", repeat: 100}, 1061 | } 1062 | 1063 | want := golden(t.Name()) 1064 | for _, tc := range testCases { 1065 | t.Run(tc.name, func(t *testing.T) { 1066 | ts := httptest.NewServer(&longResponseUnknownLengthHandler{tc.repeat}) 1067 | defer ts.Close() 1068 | logger := &Logger{ 1069 | RequestHeader: true, 1070 | RequestBody: true, 1071 | ResponseHeader: true, 1072 | ResponseBody: true, 1073 | MaxResponseBody: 10000000, 1074 | } 1075 | var buf bytes.Buffer 1076 | logger.SetOutput(&buf) 1077 | client := &http.Client{ 1078 | Transport: logger.RoundTripper(newTransport()), 1079 | } 1080 | 1081 | uri := fmt.Sprintf("%s/long-response", ts.URL) 1082 | req, err := http.NewRequest(http.MethodGet, uri, nil) 1083 | if err != nil { 1084 | t.Errorf("cannot create request: %v", err) 1085 | } 1086 | resp, err := client.Do(req) 1087 | if err != nil { 1088 | t.Errorf("cannot connect to the server: %v", err) 1089 | } 1090 | defer resp.Body.Close() 1091 | repeatedBody := strings.Repeat(petition, tc.repeat+1) 1092 | want := fmt.Sprintf(want, uri, ts.Listener.Addr(), repeatedBody) 1093 | if got := buf.String(); got != want { 1094 | t.Errorf("logged HTTP request %s; want %s", got, want) 1095 | } 1096 | testBody(t, resp.Body, []byte(repeatedBody)) 1097 | }) 1098 | } 1099 | } 1100 | 1101 | func TestOutgoingLongResponseUnknownLengthTooLong(t *testing.T) { 1102 | t.Parallel() 1103 | testCases := []struct { 1104 | name string 1105 | repeat int 1106 | max int64 1107 | }{ 1108 | {name: "short", repeat: 1, max: 4096}, 1109 | {name: "long", repeat: 100, max: 4096}, 1110 | {name: "long 1kb", repeat: 100, max: 1000}, 1111 | } 1112 | 1113 | want := golden(t.Name()) 1114 | for _, tc := range testCases { 1115 | t.Run(tc.name, func(t *testing.T) { 1116 | ts := httptest.NewServer(&longResponseUnknownLengthHandler{tc.repeat}) 1117 | defer ts.Close() 1118 | logger := &Logger{ 1119 | RequestHeader: true, 1120 | RequestBody: true, 1121 | ResponseHeader: true, 1122 | ResponseBody: true, 1123 | MaxResponseBody: tc.max, 1124 | } 1125 | var buf bytes.Buffer 1126 | logger.SetOutput(&buf) 1127 | client := &http.Client{ 1128 | Transport: logger.RoundTripper(newTransport()), 1129 | } 1130 | 1131 | uri := fmt.Sprintf("%s/long-response", ts.URL) 1132 | req, err := http.NewRequest(http.MethodGet, uri, nil) 1133 | if err != nil { 1134 | t.Errorf("cannot create request: %v", err) 1135 | } 1136 | resp, err := client.Do(req) 1137 | if err != nil { 1138 | t.Errorf("cannot connect to the server: %v", err) 1139 | } 1140 | defer resp.Body.Close() 1141 | want := fmt.Sprintf(want, uri, ts.Listener.Addr()) 1142 | want = strings.Replace(want, "(contains more than 4096 bytes)", fmt.Sprintf("(contains more than %d bytes)", logger.MaxResponseBody), 1) 1143 | if got := buf.String(); got != want { 1144 | t.Errorf("logged HTTP request %s; want %s", got, want) 1145 | } 1146 | testBody(t, resp.Body, []byte(strings.Repeat(petition, tc.repeat+1))) 1147 | }) 1148 | } 1149 | } 1150 | 1151 | func multipartTestdata(writer *multipart.Writer, body *bytes.Buffer) { 1152 | params := []struct { 1153 | name string 1154 | value string 1155 | }{ 1156 | {"author", "Frédéric Bastiat"}, 1157 | {"title", "Candlemakers' Petition"}, 1158 | } 1159 | for _, p := range params { 1160 | if err := writer.WriteField(p.name, p.value); err != nil { 1161 | panic(err) 1162 | } 1163 | } 1164 | 1165 | part, err := writer.CreateFormFile("file", "petition") 1166 | if err != nil { 1167 | panic(err) 1168 | } 1169 | if _, err = part.Write([]byte(petition)); err != nil { 1170 | panic(err) 1171 | } 1172 | if err = writer.Close(); err != nil { 1173 | panic(err) 1174 | } 1175 | } 1176 | 1177 | type multipartHandler struct { 1178 | t *testing.T 1179 | } 1180 | 1181 | func (h multipartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1182 | t := h.t 1183 | w.Header()["Date"] = nil 1184 | if err := r.ParseMultipartForm(1000); err != nil { 1185 | t.Errorf("cannot parse multipart form at server-side: %v", err) 1186 | } 1187 | if want, got := "Frédéric Bastiat", r.Form.Get("author"); want != got { 1188 | t.Errorf("got author %s, wanted %s", got, want) 1189 | } 1190 | if want, got := "Candlemakers' Petition", r.Form.Get("title"); want != got { 1191 | t.Errorf("got title %s, wanted %s", got, want) 1192 | } 1193 | 1194 | file, header, err := r.FormFile("file") 1195 | if err != nil { 1196 | t.Errorf("server cannot read file form sent over multipart: %v", err) 1197 | } 1198 | if want, got := "petition", header.Filename; want != got { 1199 | t.Errorf("got filename %s, wanted %s", header.Filename, want) 1200 | } 1201 | if header.Size != int64(len(petition)) { 1202 | t.Errorf("got size %d, wanted %d", header.Size, len(petition)) 1203 | } 1204 | 1205 | b, err := io.ReadAll(file) 1206 | if err != nil { 1207 | t.Errorf("server cannot read file sent over multipart: %v", err) 1208 | } 1209 | if string(b) != petition { 1210 | t.Error("server received different text than uploaded") 1211 | } 1212 | fmt.Fprint(w, "upload received") 1213 | } 1214 | 1215 | func TestOutgoingMultipartForm(t *testing.T) { 1216 | t.Parallel() 1217 | ts := httptest.NewServer(multipartHandler{t}) 1218 | defer ts.Close() 1219 | 1220 | logger := &Logger{ 1221 | RequestHeader: true, 1222 | // TODO(henvic): print request body once support for printing out multipart/formdata body is added. 1223 | ResponseHeader: true, 1224 | ResponseBody: true, 1225 | Formatters: []Formatter{ 1226 | &JSONFormatter{}, 1227 | }, 1228 | } 1229 | var buf bytes.Buffer 1230 | logger.SetOutput(&buf) 1231 | client := &http.Client{ 1232 | Transport: logger.RoundTripper(newTransport()), 1233 | } 1234 | 1235 | uri := fmt.Sprintf("%s/multipart-upload", ts.URL) 1236 | body := &bytes.Buffer{} 1237 | writer := multipart.NewWriter(body) 1238 | multipartTestdata(writer, body) 1239 | req, err := http.NewRequest(http.MethodPost, uri, body) 1240 | if err != nil { 1241 | t.Errorf("cannot create request: %v", err) 1242 | } 1243 | req.Header.Set("Content-Type", writer.FormDataContentType()) 1244 | if _, err = client.Do(req); err != nil { 1245 | t.Errorf("cannot connect to the server: %v", err) 1246 | } 1247 | want := fmt.Sprintf(golden(t.Name()), uri, ts.Listener.Addr(), writer.FormDataContentType()) 1248 | if got := buf.String(); got != want { 1249 | t.Errorf("logged HTTP request %s; want %s", got, want) 1250 | } 1251 | } 1252 | 1253 | func TestOutgoingProxy(t *testing.T) { 1254 | t.Parallel() 1255 | ts := httptest.NewServer(&helloHandler{}) 1256 | defer ts.Close() 1257 | 1258 | proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1259 | u, err := url.Parse(ts.URL) 1260 | if err != nil { 1261 | t.Fatal(err) 1262 | } 1263 | w.Header()["Date"] = nil 1264 | httputil.NewSingleHostReverseProxy(u).ServeHTTP(w, r) 1265 | })) 1266 | defer proxyServer.Close() 1267 | 1268 | logger := &Logger{ 1269 | RequestHeader: true, 1270 | RequestBody: true, 1271 | ResponseHeader: true, 1272 | ResponseBody: true, 1273 | } 1274 | var buf bytes.Buffer 1275 | logger.SetOutput(&buf) 1276 | client := ts.Client() 1277 | transport := client.Transport.(*http.Transport) 1278 | proxyURL, err := url.Parse(proxyServer.URL) 1279 | if err != nil { 1280 | t.Errorf("cannot parse proxy URL: %v", err) 1281 | 1282 | } 1283 | transport.Proxy = http.ProxyURL(proxyURL) 1284 | client.Transport = logger.RoundTripper(transport) 1285 | 1286 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 1287 | if err != nil { 1288 | t.Errorf("cannot create request: %v", err) 1289 | } 1290 | req.Host = "example.com" // overriding the Host header to send 1291 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 1292 | resp, err := client.Do(req) 1293 | if err != nil { 1294 | t.Errorf("cannot connect to the server: %v", err) 1295 | } 1296 | defer resp.Body.Close() 1297 | want := fmt.Sprintf(golden(t.Name()), ts.URL, proxyServer.URL) 1298 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1299 | t.Errorf("logged HTTP request %s; want %s", got, want) 1300 | } 1301 | testBody(t, resp.Body, []byte("Hello, world!")) 1302 | } 1303 | 1304 | func TestOutgoingTLS(t *testing.T) { 1305 | t.Parallel() 1306 | ts := httptest.NewTLSServer(&helloHandler{}) 1307 | defer ts.Close() 1308 | 1309 | logger := &Logger{ 1310 | TLS: true, 1311 | RequestHeader: true, 1312 | RequestBody: true, 1313 | ResponseHeader: true, 1314 | ResponseBody: true, 1315 | } 1316 | var buf bytes.Buffer 1317 | logger.SetOutput(&buf) 1318 | client := ts.Client() 1319 | client.Transport = logger.RoundTripper(client.Transport) 1320 | 1321 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 1322 | if err != nil { 1323 | t.Errorf("cannot create request: %v", err) 1324 | } 1325 | req.Host = "example.com" // overriding the Host header to send 1326 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 1327 | resp, err := client.Do(req) 1328 | if err != nil { 1329 | t.Errorf("cannot connect to the server: %v", err) 1330 | } 1331 | defer resp.Body.Close() 1332 | want := fmt.Sprintf(golden(t.Name()), ts.URL) 1333 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1334 | t.Errorf("logged HTTP request %s; want %s", got, want) 1335 | } 1336 | testBody(t, resp.Body, []byte("Hello, world!")) 1337 | } 1338 | 1339 | func TestOutgoingTLSInsecureSkipVerify(t *testing.T) { 1340 | t.Parallel() 1341 | ts := httptest.NewTLSServer(&helloHandler{}) 1342 | defer ts.Close() 1343 | 1344 | logger := &Logger{ 1345 | TLS: true, 1346 | RequestHeader: true, 1347 | RequestBody: true, 1348 | ResponseHeader: true, 1349 | ResponseBody: true, 1350 | } 1351 | var buf bytes.Buffer 1352 | logger.SetOutput(&buf) 1353 | client := ts.Client() 1354 | transport := client.Transport.(*http.Transport) 1355 | transport.TLSClientConfig.InsecureSkipVerify = true 1356 | client.Transport = logger.RoundTripper(transport) 1357 | 1358 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 1359 | if err != nil { 1360 | t.Errorf("cannot create request: %v", err) 1361 | } 1362 | req.Host = "example.com" // overriding the Host header to send 1363 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 1364 | resp, err := client.Do(req) 1365 | if err != nil { 1366 | t.Errorf("cannot connect to the server: %v", err) 1367 | } 1368 | defer resp.Body.Close() 1369 | want := fmt.Sprintf(golden(t.Name()), ts.URL) 1370 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1371 | t.Errorf("logged HTTP request %s; want %s", got, want) 1372 | } 1373 | testBody(t, resp.Body, []byte("Hello, world!")) 1374 | } 1375 | 1376 | func TestOutgoingTLSInvalidCertificate(t *testing.T) { 1377 | t.Parallel() 1378 | ts := httptest.NewTLSServer(&helloHandler{}) 1379 | ts.Config.ErrorLog = log.New(io.Discard, "", 0) 1380 | defer ts.Close() 1381 | 1382 | logger := &Logger{ 1383 | TLS: true, 1384 | RequestHeader: true, 1385 | RequestBody: true, 1386 | ResponseHeader: true, 1387 | ResponseBody: true, 1388 | } 1389 | var buf bytes.Buffer 1390 | logger.SetOutput(&buf) 1391 | client := &http.Client{ 1392 | Transport: logger.RoundTripper(newTransport()), 1393 | } 1394 | 1395 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 1396 | if err != nil { 1397 | t.Errorf("cannot create request: %v", err) 1398 | } 1399 | req.Host = "example.com" // overriding the Host header to send 1400 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 1401 | if _, err = client.Do(req); err == nil || !strings.Contains(err.Error(), "x509") { 1402 | t.Errorf("cannot connect to the server has unexpected error: %v", err) 1403 | } 1404 | want := fmt.Sprintf(golden(t.Name()), ts.URL) 1405 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1406 | t.Errorf("logged HTTP request %s; want %s", got, want) 1407 | } 1408 | } 1409 | 1410 | func TestOutgoingTLSBadClientCertificate(t *testing.T) { 1411 | t.Parallel() 1412 | ts := httptest.NewUnstartedServer(&helloHandler{}) 1413 | ts.TLS = &tls.Config{ 1414 | ClientAuth: tls.RequireAndVerifyClientCert, 1415 | } 1416 | ts.StartTLS() 1417 | defer ts.Close() 1418 | 1419 | logger := &Logger{ 1420 | TLS: true, 1421 | RequestHeader: true, 1422 | RequestBody: true, 1423 | ResponseHeader: true, 1424 | ResponseBody: true, 1425 | } 1426 | var buf bytes.Buffer 1427 | logger.SetOutput(&buf) 1428 | ts.Config.ErrorLog = log.New(io.Discard, "", 0) 1429 | client := ts.Client() 1430 | cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") 1431 | if err != nil { 1432 | panic(err) 1433 | } 1434 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 1435 | if err != nil { 1436 | t.Errorf("failed to parse certificate for copying Leaf field") 1437 | } 1438 | transport := client.Transport.(*http.Transport) 1439 | transport.TLSClientConfig.Certificates = []tls.Certificate{ 1440 | cert, 1441 | } 1442 | client.Transport = logger.RoundTripper(transport) 1443 | 1444 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 1445 | if err != nil { 1446 | t.Errorf("cannot create request: %v", err) 1447 | } 1448 | req.Host = "example.com" // overriding the Host header to send 1449 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 1450 | var ue = &url.Error{} 1451 | if _, err = client.Do(req); err == nil || !errors.As(err, &ue) { 1452 | t.Errorf("got: %v, expected bad certificate error message", err) 1453 | } 1454 | want := fmt.Sprintf(golden(t.Name()), ts.URL, strings.SplitAfter(err.Error(), "remote error: tls: ")[1]) 1455 | if got := buf.String(); !strings.Contains(got, want) { 1456 | t.Errorf("logged HTTP request %s; want %s", got, want) 1457 | } 1458 | } 1459 | 1460 | func TestOutgoingHTTP2MutualTLS(t *testing.T) { 1461 | t.Parallel() 1462 | caCert, err := os.ReadFile("testdata/cert.pem") 1463 | if err != nil { 1464 | panic(err) 1465 | } 1466 | clientCert, err := os.ReadFile("testdata/cert-client.pem") 1467 | if err != nil { 1468 | panic(err) 1469 | } 1470 | caCertPool := x509.NewCertPool() 1471 | caCertPool.AppendCertsFromPEM(caCert) 1472 | caCertPool.AppendCertsFromPEM(clientCert) 1473 | tlsConfig := &tls.Config{ 1474 | ClientCAs: caCertPool, 1475 | ClientAuth: tls.RequireAndVerifyClientCert, 1476 | } 1477 | 1478 | // NOTE(henvic): Using httptest directly turned out complicated. 1479 | // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go 1480 | server := &http.Server{ 1481 | TLSConfig: tlsConfig, 1482 | Handler: &helloHandler{}, 1483 | } 1484 | listener, err := netListener() 1485 | if err != nil { 1486 | panic(fmt.Sprintf("failed to listen on a port: %v", err)) 1487 | } 1488 | defer listener.Close() 1489 | go func() { 1490 | // Certificate generated with 1491 | // $ openssl req -x509 -newkey rsa:2048 \ 1492 | // -new -nodes -sha256 \ 1493 | // -days 36500 \ 1494 | // -out cert.pem \ 1495 | // -keyout key.pem \ 1496 | // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ 1497 | // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") 1498 | if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { 1499 | t.Errorf("server exit with unexpected error: %v", errcp) 1500 | } 1501 | }() 1502 | defer server.Shutdown(context.Background()) 1503 | 1504 | // Certificate generated with 1505 | // $ openssl req -newkey rsa:2048 \ 1506 | // -new -nodes -x509 \ 1507 | // -days 36500 \ 1508 | // -out cert-client.pem \ 1509 | // -keyout key-client.pem \ 1510 | // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" 1511 | cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") 1512 | if err != nil { 1513 | t.Errorf("failed to load X509 key pair: %v", err) 1514 | } 1515 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 1516 | if err != nil { 1517 | t.Errorf("failed to parse certificate for copying Leaf field") 1518 | } 1519 | 1520 | // Create a HTTPS client and supply the created CA pool and certificate 1521 | clientTLSConfig := &tls.Config{ 1522 | RootCAs: caCertPool, 1523 | Certificates: []tls.Certificate{cert}, 1524 | } 1525 | transport := newTransport() 1526 | transport.TLSClientConfig = clientTLSConfig 1527 | client := &http.Client{ 1528 | Transport: transport, 1529 | } 1530 | logger := &Logger{ 1531 | TLS: true, 1532 | RequestHeader: true, 1533 | RequestBody: true, 1534 | ResponseHeader: true, 1535 | ResponseBody: true, 1536 | } 1537 | var buf bytes.Buffer 1538 | logger.SetOutput(&buf) 1539 | client.Transport = logger.RoundTripper(client.Transport) 1540 | _, port, err := net.SplitHostPort(listener.Addr().String()) 1541 | if err != nil { 1542 | panic(err) 1543 | } 1544 | 1545 | var host = fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) 1546 | resp, err := client.Get(host) 1547 | if err != nil { 1548 | t.Errorf("cannot create request: %v", err) 1549 | } 1550 | defer resp.Body.Close() 1551 | testBody(t, resp.Body, []byte("Hello, world!")) 1552 | want := fmt.Sprintf(golden(t.Name()), host, port) 1553 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1554 | t.Errorf("logged HTTP request %s; want %s", got, want) 1555 | } 1556 | } 1557 | 1558 | func TestOutgoingHTTP2MutualTLSNoSafetyLogging(t *testing.T) { 1559 | t.Parallel() 1560 | caCert, err := os.ReadFile("testdata/cert.pem") 1561 | if err != nil { 1562 | panic(err) 1563 | } 1564 | clientCert, err := os.ReadFile("testdata/cert-client.pem") 1565 | if err != nil { 1566 | panic(err) 1567 | } 1568 | caCertPool := x509.NewCertPool() 1569 | caCertPool.AppendCertsFromPEM(caCert) 1570 | caCertPool.AppendCertsFromPEM(clientCert) 1571 | tlsConfig := &tls.Config{ 1572 | ClientCAs: caCertPool, 1573 | ClientAuth: tls.RequireAndVerifyClientCert, 1574 | } 1575 | 1576 | // NOTE(henvic): Using httptest directly turned out complicated. 1577 | // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go 1578 | server := &http.Server{ 1579 | TLSConfig: tlsConfig, 1580 | Handler: &helloHandler{}, 1581 | } 1582 | listener, err := netListener() 1583 | if err != nil { 1584 | panic(fmt.Sprintf("failed to listen on a port: %v", err)) 1585 | } 1586 | defer listener.Close() 1587 | go func() { 1588 | // Certificate generated with 1589 | // $ openssl req -x509 -newkey rsa:2048 \ 1590 | // -new -nodes -sha256 \ 1591 | // -days 36500 \ 1592 | // -out cert.pem \ 1593 | // -keyout key.pem \ 1594 | // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ 1595 | // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") 1596 | if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { 1597 | t.Errorf("server exit with unexpected error: %v", errcp) 1598 | } 1599 | }() 1600 | defer server.Shutdown(context.Background()) 1601 | 1602 | // Certificate generated with 1603 | // $ openssl req -newkey rsa:2048 \ 1604 | // -new -nodes -x509 \ 1605 | // -days 36500 \ 1606 | // -out cert-client.pem \ 1607 | // -keyout key-client.pem \ 1608 | // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" 1609 | cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") 1610 | if err != nil { 1611 | t.Errorf("failed to load X509 key pair: %v", err) 1612 | } 1613 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 1614 | if err != nil { 1615 | t.Errorf("failed to parse certificate for copying Leaf field") 1616 | } 1617 | 1618 | // Create a HTTPS client and supply the created CA pool and certificate 1619 | clientTLSConfig := &tls.Config{ 1620 | RootCAs: caCertPool, 1621 | Certificates: []tls.Certificate{cert}, 1622 | } 1623 | transport := newTransport() 1624 | transport.TLSClientConfig = clientTLSConfig 1625 | client := &http.Client{ 1626 | Transport: transport, 1627 | } 1628 | logger := &Logger{ 1629 | // TLS must be false 1630 | RequestHeader: true, 1631 | RequestBody: true, 1632 | ResponseHeader: true, 1633 | ResponseBody: true, 1634 | } 1635 | var buf bytes.Buffer 1636 | logger.SetOutput(&buf) 1637 | client.Transport = logger.RoundTripper(client.Transport) 1638 | _, port, err := net.SplitHostPort(listener.Addr().String()) 1639 | if err != nil { 1640 | panic(err) 1641 | } 1642 | var host = fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) 1643 | resp, err := client.Get(host) 1644 | if err != nil { 1645 | t.Errorf("cannot create request: %v", err) 1646 | } 1647 | defer resp.Body.Close() 1648 | testBody(t, resp.Body, []byte("Hello, world!")) 1649 | want := fmt.Sprintf(golden(t.Name()), host, port) 1650 | if got := buf.String(); got != want { 1651 | t.Errorf("logged HTTP request %s; want %s", got, want) 1652 | } 1653 | } 1654 | 1655 | // netListener is similar to httptest.newlocalListener() and listens locally in a random port. 1656 | // See https://github.com/golang/go/blob/5375c71289917ac7b25c6fa4bb0f4fa17be19a07/src/net/http/httptest/server.go#L60-L75 1657 | func netListener() (net.Listener, error) { 1658 | listener, err := net.Listen("tcp", "127.0.0.1:0") 1659 | if err != nil { 1660 | return net.Listen("tcp6", "[::1]:0") 1661 | } 1662 | return listener, nil 1663 | } 1664 | -------------------------------------------------------------------------------- /example/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/henvic/httpretty" 9 | ) 10 | 11 | func main() { 12 | logger := &httpretty.Logger{ 13 | Time: true, 14 | TLS: true, 15 | RequestHeader: true, 16 | RequestBody: true, 17 | ResponseHeader: true, 18 | ResponseBody: true, 19 | Colors: true, // erase line if you don't like colors 20 | Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, 21 | } 22 | // Set the default HTTP client to use the logger RoundTripper. 23 | http.DefaultClient.Transport = logger.RoundTripper(http.DefaultTransport) 24 | 25 | // Example of request. 26 | if _, err := http.Get("https://www.google.com/"); err != nil { 27 | fmt.Fprintf(os.Stderr, "%+v\n", err) 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/httprepl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/henvic/httpretty" 13 | ) 14 | 15 | func main() { 16 | logger := &httpretty.Logger{ 17 | Time: true, 18 | TLS: true, 19 | RequestHeader: true, 20 | RequestBody: true, 21 | ResponseHeader: true, 22 | ResponseBody: true, 23 | Colors: true, // erase line if you don't like colors 24 | Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, 25 | } 26 | 27 | // Using a custom HTTP client using the logger RoundTripper, rather than http.DefaultClient. 28 | client := &http.Client{ 29 | Transport: logger.RoundTripper(http.DefaultTransport), 30 | } 31 | 32 | fmt.Print("httprepl is a small HTTP client REPL (read-eval-print-loop) program example\n\n") 33 | help() 34 | reader := bufio.NewReader(os.Stdin) 35 | for { 36 | fmt.Print("$ ") 37 | readEvalPrint(reader, client) 38 | } 39 | } 40 | 41 | func readEvalPrint(reader *bufio.Reader, client *http.Client) { 42 | s, err := reader.ReadString('\n') 43 | if err != nil { 44 | fmt.Fprintf(os.Stderr, "cannot read stdin: %v\n", err) 45 | os.Exit(1) 46 | } 47 | 48 | if runtime.GOOS == "windows" { 49 | s = strings.TrimRight(s, "\r\n") 50 | } else { 51 | s = strings.TrimRight(s, "\n") 52 | } 53 | s = strings.TrimSpace(s) 54 | 55 | switch { 56 | case s == "exit": 57 | os.Exit(0) 58 | case s == "help": 59 | help() 60 | return 61 | case s == "": 62 | return 63 | case s == "get": 64 | fmt.Fprintln(os.Stderr, "missing address") 65 | case !strings.HasPrefix(s, "get "): 66 | fmt.Fprint(os.Stderr, "invalid command\n\n") 67 | return 68 | } 69 | 70 | s = strings.TrimPrefix(s, "get ") 71 | uri, err := url.Parse(s) 72 | if err == nil && uri.Scheme == "" { 73 | uri.Scheme = "http" 74 | s = uri.String() 75 | } 76 | 77 | // we just ignore the request contents but you can see it printed thanks to the logger. 78 | if _, err := client.Get(s); err != nil { 79 | fmt.Fprintf(os.Stderr, "%+v\n\n", err) 80 | } 81 | fmt.Println() 82 | } 83 | 84 | func help() { 85 | fmt.Print(`Commands available: 86 | get
URL to get. Example: "get www.google.com" 87 | help This command list 88 | exit Quit the application 89 | 90 | `) 91 | } 92 | -------------------------------------------------------------------------------- /example/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/henvic/httpretty" 9 | ) 10 | 11 | func main() { 12 | logger := &httpretty.Logger{ 13 | Time: true, 14 | TLS: true, 15 | RequestHeader: true, 16 | RequestBody: true, 17 | ResponseHeader: true, 18 | ResponseBody: true, 19 | Colors: true, // erase line if you don't like colors 20 | } 21 | 22 | addr := ":8090" 23 | fmt.Printf("Open http://localhost%s in the browser.\n", addr) 24 | /* #nosec G114 Ignore timeout */ 25 | if err := http.ListenAndServe(addr, logger.Middleware(helloHandler{})); err != http.ErrServerClosed { 26 | fmt.Fprintln(os.Stderr, err) 27 | } 28 | } 29 | 30 | type helloHandler struct{} 31 | 32 | func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 | w.Header()["Date"] = nil 34 | fmt.Fprintf(w, "Hello, world!") 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/henvic/httpretty 2 | 3 | go 1.22 4 | 5 | require golang.org/x/tools v0.14.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 4 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 5 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 6 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 7 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 8 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 9 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 10 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 11 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 12 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 13 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 14 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 18 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 19 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 21 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 22 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 23 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 24 | golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= 25 | golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= 26 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 27 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 29 | -------------------------------------------------------------------------------- /httpretty.go: -------------------------------------------------------------------------------- 1 | // Package httpretty prints your HTTP requests pretty on your terminal screen. 2 | // You can use this package both on the client-side and on the server-side. 3 | // 4 | // This package provides a better way to view HTTP traffic without httputil 5 | // DumpRequest, DumpRequestOut, and DumpResponse heavy debugging functions. 6 | // 7 | // You can use the logger quickly to log requests you are opening. For example: 8 | // 9 | // package main 10 | // 11 | // import ( 12 | // "fmt" 13 | // "net/http" 14 | // "os" 15 | // 16 | // "github.com/henvic/httpretty" 17 | // ) 18 | // 19 | // func main() { 20 | // logger := &httpretty.Logger{ 21 | // Time: true, 22 | // TLS: true, 23 | // RequestHeader: true, 24 | // RequestBody: true, 25 | // ResponseHeader: true, 26 | // ResponseBody: true, 27 | // Colors: true, 28 | // Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, 29 | // } 30 | // 31 | // http.DefaultClient.Transport = logger.RoundTripper(http.DefaultClient.Transport) // tip: you can use it on any *http.Client 32 | // 33 | // if _, err := http.Get("https://www.google.com/"); err != nil { 34 | // fmt.Fprintf(os.Stderr, "%+v\n", err) 35 | // os.Exit(1) 36 | // } 37 | // } 38 | // 39 | // If you pass nil to the logger.RoundTripper it is going to fallback to http.DefaultTransport. 40 | // 41 | // You can use the logger quickly to log requests on your server. For example: 42 | // 43 | // logger := &httpretty.Logger{ 44 | // Time: true, 45 | // TLS: true, 46 | // RequestHeader: true, 47 | // RequestBody: true, 48 | // ResponseHeader: true, 49 | // ResponseBody: true, 50 | // } 51 | // 52 | // logger.Middleware(handler) 53 | // 54 | // Note: server logs don't include response headers set by the server. 55 | // Client logs don't include request headers set by the HTTP client. 56 | package httpretty 57 | 58 | import ( 59 | "bytes" 60 | "context" 61 | "crypto/tls" 62 | "encoding/json" 63 | "errors" 64 | "io" 65 | "net/http" 66 | "net/textproto" 67 | "os" 68 | "regexp" 69 | "sync" 70 | 71 | "github.com/henvic/httpretty/internal/color" 72 | ) 73 | 74 | // Formatter can be used to format body. 75 | // 76 | // If the Format function returns an error, the content is printed in verbatim after a warning. 77 | // Match receives a media type from the Content-Type field. The body is formatted if it returns true. 78 | type Formatter interface { 79 | Match(mediatype string) bool 80 | Format(w io.Writer, src []byte) error 81 | } 82 | 83 | // WithHide can be used to protect a request from being exposed. 84 | func WithHide(ctx context.Context) context.Context { 85 | return context.WithValue(ctx, contextHide{}, struct{}{}) 86 | } 87 | 88 | // Logger provides a way for you to print client and server-side information about your HTTP traffic. 89 | type Logger struct { 90 | // SkipRequestInfo avoids printing a line showing the request URI on all requests plus a line 91 | // containing the remote address on server-side requests. 92 | SkipRequestInfo bool 93 | 94 | // Time the request began and its duration. 95 | Time bool 96 | 97 | // TLS information, such as certificates and ciphers. 98 | // BUG(henvic): Currently, the TLS information prints after the response header, although it 99 | // should be printed before the request header. 100 | TLS bool 101 | 102 | // RequestHeader set by the client or received from the server. 103 | RequestHeader bool 104 | 105 | // RequestBody sent by the client or received by the server. 106 | RequestBody bool 107 | 108 | // ResponseHeader received by the client or set by the HTTP handlers. 109 | ResponseHeader bool 110 | 111 | // ResponseBody received by the client or set by the server. 112 | ResponseBody bool 113 | 114 | // SkipSanitize bypasses sanitizing headers containing credentials (such as Authorization). 115 | SkipSanitize bool 116 | 117 | // Colors set ANSI escape codes that terminals use to print text in different colors. 118 | Colors bool 119 | 120 | // Align HTTP headers. 121 | Align bool 122 | 123 | // Formatters for the request and response bodies. 124 | // No standard formatters are used. You need to add what you want to use explicitly. 125 | // We provide a JSONFormatter for convenience (add it manually). 126 | Formatters []Formatter 127 | 128 | // MaxRequestBody the logger can print. 129 | // If value is not set and Content-Length is not sent, 4096 bytes is considered. 130 | MaxRequestBody int64 131 | 132 | // MaxResponseBody the logger can print. 133 | // If value is not set and Content-Length is not sent, 4096 bytes is considered. 134 | MaxResponseBody int64 135 | 136 | mu sync.Mutex // ensures atomic writes; protects the following fields 137 | w io.Writer 138 | filter Filter 139 | skipHeader map[string]struct{} 140 | bodyFilter BodyFilter 141 | flusher Flusher 142 | } 143 | 144 | // Filter allows you to skip requests. 145 | // 146 | // If an error happens and you want to log it, you can pass a not-null error value. 147 | type Filter func(req *http.Request) (skip bool, err error) 148 | 149 | // BodyFilter allows you to skip printing a HTTP body based on its associated Header. 150 | // 151 | // It can be used for omitting HTTP Request and Response bodies. 152 | // You can filter by checking properties such as Content-Type or Content-Length. 153 | // 154 | // On a HTTP server, this function is called even when no body is present due to 155 | // http.Request always carrying a non-nil value. 156 | type BodyFilter func(h http.Header) (skip bool, err error) 157 | 158 | // Flusher defines how logger prints requests. 159 | type Flusher int 160 | 161 | // Logger can print without flushing, when they are available, or when the request is done. 162 | const ( 163 | // NoBuffer strategy prints anything immediately, without buffering. 164 | // It has the issue of mingling concurrent requests in unpredictable ways. 165 | NoBuffer Flusher = iota 166 | 167 | // OnReady buffers and prints each step of the request or response (header, body) whenever they are ready. 168 | // It reduces mingling caused by mingling but does not give any ordering guarantee, so responses can still be out of order. 169 | OnReady 170 | 171 | // OnEnd buffers the whole request and flushes it once, in the end. 172 | OnEnd 173 | ) 174 | 175 | // SetFilter allows you to set a function to skip requests. 176 | // Pass nil to remove the filter. This method is concurrency safe. 177 | func (l *Logger) SetFilter(f Filter) { 178 | l.mu.Lock() 179 | defer l.mu.Unlock() 180 | l.filter = f 181 | } 182 | 183 | // SkipHeader allows you to skip printing specific headers. 184 | // This method is concurrency safe. 185 | func (l *Logger) SkipHeader(headers []string) { 186 | l.mu.Lock() 187 | defer l.mu.Unlock() 188 | m := map[string]struct{}{} 189 | for _, h := range headers { 190 | m[textproto.CanonicalMIMEHeaderKey(h)] = struct{}{} 191 | } 192 | l.skipHeader = m 193 | } 194 | 195 | // SetBodyFilter allows you to set a function to skip printing a body. 196 | // Pass nil to remove the body filter. This method is concurrency safe. 197 | func (l *Logger) SetBodyFilter(f BodyFilter) { 198 | l.mu.Lock() 199 | defer l.mu.Unlock() 200 | l.bodyFilter = f 201 | } 202 | 203 | // SetOutput sets the output destination for the logger. 204 | func (l *Logger) SetOutput(w io.Writer) { 205 | l.mu.Lock() 206 | defer l.mu.Unlock() 207 | l.w = w 208 | } 209 | 210 | // SetFlusher sets the flush strategy for the logger. 211 | func (l *Logger) SetFlusher(f Flusher) { 212 | l.mu.Lock() 213 | defer l.mu.Unlock() 214 | l.flusher = f 215 | } 216 | 217 | func (l *Logger) getWriter() io.Writer { 218 | if l.w == nil { 219 | return os.Stdout 220 | } 221 | 222 | return l.w 223 | } 224 | 225 | func (l *Logger) getFilter() Filter { 226 | l.mu.Lock() 227 | f := l.filter 228 | defer l.mu.Unlock() 229 | return f 230 | } 231 | 232 | func (l *Logger) getBodyFilter() BodyFilter { 233 | l.mu.Lock() 234 | f := l.bodyFilter 235 | defer l.mu.Unlock() 236 | return f 237 | } 238 | 239 | func (l *Logger) cloneSkipHeader() map[string]struct{} { 240 | l.mu.Lock() 241 | skipped := l.skipHeader 242 | l.mu.Unlock() 243 | 244 | m := map[string]struct{}{} 245 | for h := range skipped { 246 | m[h] = struct{}{} 247 | } 248 | 249 | return m 250 | } 251 | 252 | type contextHide struct{} 253 | 254 | type roundTripper struct { 255 | logger *Logger 256 | rt http.RoundTripper 257 | } 258 | 259 | // RoundTripper returns a RoundTripper that uses the logger. 260 | func (l *Logger) RoundTripper(rt http.RoundTripper) http.RoundTripper { 261 | return roundTripper{ 262 | logger: l, 263 | rt: rt, 264 | } 265 | } 266 | 267 | // RoundTrip implements the http.RoundTrip interface. 268 | func (r roundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { 269 | tripper := r.rt 270 | if tripper == nil { 271 | // BUG(henvic): net/http data race condition when the client 272 | // does concurrent requests using the very same HTTP transport. 273 | // See Go standard library issue https://golang.org/issue/30597 274 | tripper = http.RoundTripper(http.DefaultTransport) 275 | } 276 | l := r.logger 277 | p := newPrinter(l) 278 | defer p.flush() 279 | if hide := req.Context().Value(contextHide{}); hide != nil || p.checkFilter(req) { 280 | return tripper.RoundTrip(req) 281 | } 282 | var tlsClientConfig *tls.Config 283 | if l.Time { 284 | defer p.printTimeRequest()() 285 | } 286 | if !l.SkipRequestInfo { 287 | p.printRequestInfo(req) 288 | } 289 | // Try to get some information from transport 290 | transport, ok := tripper.(*http.Transport) 291 | // If proxy is used, then print information about proxy server 292 | if ok && transport.Proxy != nil { 293 | proxyUrl, err := transport.Proxy(req) 294 | if proxyUrl != nil && err == nil { 295 | p.printf("* Using proxy: %s\n", p.format(color.FgBlue, proxyUrl.String())) 296 | } 297 | } 298 | if ok && transport.TLSClientConfig != nil { 299 | tlsClientConfig = transport.TLSClientConfig 300 | if tlsClientConfig.InsecureSkipVerify { 301 | p.printf("* Skipping TLS verification: %s\n", 302 | p.format(color.FgRed, "connection is susceptible to man-in-the-middle attacks.")) 303 | } 304 | } 305 | // Maybe print outgoing TLS information. 306 | if l.TLS && tlsClientConfig != nil { 307 | // please remember http.Request.TLS is ignored by the HTTP client. 308 | p.printOutgoingClientTLS(tlsClientConfig) 309 | } 310 | p.printRequest(req) 311 | defer func() { 312 | if err != nil { 313 | p.printf("* %s\n", p.format(color.FgRed, err.Error())) 314 | if resp == nil { 315 | return 316 | } 317 | } 318 | if l.TLS { 319 | p.printTLSInfo(resp.TLS, false) 320 | p.printTLSServer(req.Host, resp.TLS) 321 | } 322 | p.printResponse(resp) 323 | }() 324 | return tripper.RoundTrip(req) 325 | } 326 | 327 | // Middleware for logging incoming requests to a HTTP server. 328 | func (l *Logger) Middleware(next http.Handler) http.Handler { 329 | return httpHandler{ 330 | logger: l, 331 | next: next, 332 | } 333 | } 334 | 335 | type httpHandler struct { 336 | logger *Logger 337 | next http.Handler 338 | } 339 | 340 | // ServeHTTP is a middleware for logging incoming requests to a HTTP server. 341 | func (h httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 342 | l := h.logger 343 | p := newPrinter(l) 344 | defer p.flush() 345 | if hide := req.Context().Value(contextHide{}); hide != nil || p.checkFilter(req) { 346 | h.next.ServeHTTP(w, req) 347 | return 348 | } 349 | if p.logger.Time { 350 | defer p.printTimeRequest()() 351 | } 352 | if !p.logger.SkipRequestInfo { 353 | p.printRequestInfo(req) 354 | } 355 | if p.logger.TLS { 356 | p.printTLSInfo(req.TLS, true) 357 | p.printIncomingClientTLS(req.TLS) 358 | } 359 | p.printRequest(req) 360 | rec := &responseRecorder{ 361 | ResponseWriter: w, 362 | statusCode: http.StatusOK, 363 | maxReadableBody: l.MaxResponseBody, 364 | buf: &bytes.Buffer{}, 365 | } 366 | defer p.printServerResponse(req, rec) 367 | h.next.ServeHTTP(rec, req) 368 | } 369 | 370 | // PrintRequest prints a request, even when WithHide is used to hide it. 371 | // 372 | // It doesn't log TLS connection details or request duration. 373 | func (l *Logger) PrintRequest(req *http.Request) { 374 | var p = printer{logger: l} 375 | if skip := p.checkFilter(req); skip { 376 | return 377 | } 378 | p.printRequest(req) 379 | } 380 | 381 | // PrintResponse prints a response. 382 | func (l *Logger) PrintResponse(resp *http.Response) { 383 | var p = printer{logger: l} 384 | p.printResponse(resp) 385 | } 386 | 387 | // JSONFormatter helps you read unreadable JSON documents. 388 | // 389 | // github.com/tidwall/pretty could be used to add colors to it. 390 | // However, it would add an external dependency. If you want, you can define 391 | // your own formatter using it or anything else. See Formatter. 392 | type JSONFormatter struct{} 393 | 394 | // jsonTypeRE can be used to identify JSON media types, such as 395 | // application/json or application/vnd.api+json. 396 | // 397 | // Source: https://github.com/cli/cli/blob/63a4319f6caedccbadf1bf0317d70b6f0cb1b5b9/internal/authflow/flow.go#L27 398 | var jsonTypeRE = regexp.MustCompile(`[/+]json($|;)`) 399 | 400 | // Match JSON media type. 401 | func (j *JSONFormatter) Match(mediatype string) bool { 402 | return jsonTypeRE.MatchString(mediatype) 403 | } 404 | 405 | // Format JSON content. 406 | func (j *JSONFormatter) Format(w io.Writer, src []byte) error { 407 | if !json.Valid(src) { 408 | // We want to get the error of json.checkValid, not unmarshal it. 409 | // The happy path has been optimized, maybe prematurely. 410 | if err := json.Unmarshal(src, &json.RawMessage{}); err != nil { 411 | return err 412 | } 413 | } 414 | // avoiding allocation as we use *bytes.Buffer to store the formatted body before printing 415 | dst, ok := w.(*bytes.Buffer) 416 | if !ok { 417 | // mitigating panic to avoid upsetting anyone who uses this directly 418 | return errors.New("underlying writer for JSONFormatter must be *bytes.Buffer") 419 | } 420 | return json.Indent(dst, src, "", " ") 421 | } 422 | -------------------------------------------------------------------------------- /httpretty_test.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | // race is a flag that can be usde to detect whether the race detector is on. 16 | // It was added because as of Go 1.13.7 the TestOutgoingConcurrency test is failing because of a bug on the 17 | // net/http standard library package. 18 | // See race_test.go. 19 | // See https://golang.org/issue/30597 20 | var race bool 21 | 22 | // sample from http://bastiat.org/fr/petition.html 23 | // 24 | //go:embed testdata/petition.golden 25 | var petition string 26 | 27 | func TestPrintRequest(t *testing.T) { 28 | t.Parallel() 29 | var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | logger := &Logger{ 35 | TLS: true, 36 | RequestHeader: true, 37 | RequestBody: true, 38 | ResponseHeader: true, 39 | ResponseBody: true, 40 | } 41 | var buf bytes.Buffer 42 | logger.SetOutput(&buf) 43 | logger.PrintRequest(req) 44 | 45 | want := `> POST / HTTP/1.1 46 | > Host: wxww.example.com 47 | 48 | ` 49 | if got := buf.String(); got != want { 50 | t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) 51 | } 52 | } 53 | 54 | func TestPrintRequestWithAlign(t *testing.T) { 55 | t.Parallel() 56 | var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) 57 | if err != nil { 58 | panic(err) 59 | } 60 | req.Header.Set("Header", "foo") 61 | req.Header.Set("Other-Header", "bar") 62 | 63 | logger := &Logger{ 64 | TLS: true, 65 | RequestHeader: true, 66 | RequestBody: true, 67 | ResponseHeader: true, 68 | ResponseBody: true, 69 | Align: true, 70 | } 71 | var buf bytes.Buffer 72 | logger.SetOutput(&buf) 73 | logger.PrintRequest(req) 74 | 75 | want := `> POST / HTTP/1.1 76 | > Host: wxww.example.com 77 | > Header: foo 78 | > Other-Header: bar 79 | 80 | ` 81 | if got := buf.String(); got != want { 82 | t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) 83 | } 84 | } 85 | 86 | func TestPrintRequestWithColors(t *testing.T) { 87 | t.Parallel() 88 | var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | logger := &Logger{ 94 | TLS: true, 95 | RequestHeader: true, 96 | RequestBody: true, 97 | ResponseHeader: true, 98 | ResponseBody: true, 99 | Colors: true, 100 | } 101 | var buf bytes.Buffer 102 | logger.SetOutput(&buf) 103 | logger.PrintRequest(req) 104 | want := "> \x1b[34;1mPOST\x1b[0m \x1b[33m/\x1b[0m \x1b[34mHTTP/1.1\x1b[0m" + 105 | "\n> \x1b[34;1mHost\x1b[0m\x1b[31m:\x1b[0m \x1b[33mwxww.example.com\x1b[0m\n\n" 106 | if got := buf.String(); got != want { 107 | t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) 108 | } 109 | } 110 | 111 | func TestEncodingQueryStringParams(t *testing.T) { 112 | // Regression test for verifying query string parameters are being encoded correctly when printing with colors. 113 | // Issue reported by @mislav in https://github.com/henvic/httpretty/issues/9. 114 | t.Parallel() 115 | qs := url.Values{} 116 | qs.Set("a", "b") 117 | qs.Set("i", "j") 118 | qs.Set("x", "y") 119 | qs.Set("z", "+=") 120 | qs.Set("var", "foo&bar") 121 | u := url.URL{ 122 | Scheme: "http", 123 | Host: "www.example.com", 124 | Path: "/mypath", 125 | RawQuery: qs.Encode(), 126 | } 127 | var req, err = http.NewRequest(http.MethodPost, u.String(), nil) 128 | if err != nil { 129 | panic(err) 130 | } 131 | 132 | logger := &Logger{ 133 | TLS: true, 134 | RequestHeader: true, 135 | RequestBody: true, 136 | ResponseHeader: true, 137 | ResponseBody: true, 138 | Colors: true, 139 | } 140 | var buf bytes.Buffer 141 | logger.SetOutput(&buf) 142 | logger.PrintRequest(req) 143 | want := "> \x1b[34;1mPOST\x1b[0m \x1b[33m/mypath?a=b&i=j&var=foo%26bar&x=y&z=%2B%3D\x1b[0m \x1b[34mHTTP/1.1\x1b[0m" + 144 | "\n> \x1b[34;1mHost\x1b[0m\x1b[31m:\x1b[0m \x1b[33mwww.example.com\x1b[0m\n\n" 145 | if got := buf.String(); got != want { 146 | t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) 147 | } 148 | } 149 | 150 | func TestEncodingQueryStringParamsNoColors(t *testing.T) { 151 | t.Parallel() 152 | qs := url.Values{} 153 | qs.Set("a", "b") 154 | qs.Set("i", "j") 155 | qs.Set("x", "y") 156 | qs.Set("z", "+=") 157 | qs.Set("var", "foo&bar") 158 | u := url.URL{ 159 | Scheme: "http", 160 | Host: "www.example.com", 161 | Path: "/mypath", 162 | RawQuery: qs.Encode(), 163 | } 164 | var req, err = http.NewRequest(http.MethodPost, u.String(), nil) 165 | if err != nil { 166 | panic(err) 167 | } 168 | 169 | logger := &Logger{ 170 | TLS: true, 171 | RequestHeader: true, 172 | RequestBody: true, 173 | ResponseHeader: true, 174 | ResponseBody: true, 175 | } 176 | var buf bytes.Buffer 177 | logger.SetOutput(&buf) 178 | logger.PrintRequest(req) 179 | want := `> POST /mypath?a=b&i=j&var=foo%26bar&x=y&z=%2B%3D HTTP/1.1 180 | > Host: www.example.com 181 | 182 | ` 183 | if got := buf.String(); got != want { 184 | t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) 185 | } 186 | } 187 | 188 | func TestPrintRequestFiltered(t *testing.T) { 189 | t.Parallel() 190 | var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) 191 | if err != nil { 192 | panic(err) 193 | } 194 | 195 | logger := &Logger{ 196 | TLS: true, 197 | RequestHeader: true, 198 | RequestBody: true, 199 | ResponseHeader: true, 200 | ResponseBody: true, 201 | } 202 | var buf bytes.Buffer 203 | logger.SetOutput(&buf) 204 | logger.SetFilter(func(req *http.Request) (skip bool, err error) { 205 | return true, nil 206 | }) 207 | logger.PrintRequest(req) 208 | if got := buf.Len(); got != 0 { 209 | t.Errorf("got %v from logger, wanted nothing (everything should be filtered)", got) 210 | } 211 | } 212 | 213 | func TestPrintRequestNil(t *testing.T) { 214 | t.Parallel() 215 | logger := &Logger{ 216 | TLS: true, 217 | RequestHeader: true, 218 | RequestBody: true, 219 | ResponseHeader: true, 220 | ResponseBody: true, 221 | } 222 | 223 | var buf bytes.Buffer 224 | logger.SetOutput(&buf) 225 | logger.PrintRequest(nil) 226 | want := "> error: null request\n" 227 | if got := buf.String(); got != want { 228 | t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) 229 | } 230 | } 231 | 232 | func TestPrintResponseNil(t *testing.T) { 233 | t.Parallel() 234 | logger := &Logger{ 235 | TLS: true, 236 | RequestHeader: true, 237 | RequestBody: true, 238 | ResponseHeader: true, 239 | ResponseBody: true, 240 | } 241 | var buf bytes.Buffer 242 | logger.SetOutput(&buf) 243 | logger.PrintResponse(nil) 244 | 245 | want := "< error: null response\n" 246 | if got := buf.String(); got != want { 247 | t.Errorf("PrintResponse(req) = %v, wanted %v", got, want) 248 | } 249 | } 250 | 251 | func testBody(t *testing.T, r io.Reader, want []byte) { 252 | t.Helper() 253 | got, err := io.ReadAll(r) 254 | if err != nil { 255 | t.Errorf("expected no error reading response body, got %v instead", err) 256 | } 257 | if !bytes.Equal(got, want) { 258 | if len(got) != len(want) && (len(got) > 100 || len(want) > 100) { 259 | t.Errorf(`got body length = %v, wanted %v`, len(got), len(want)) 260 | } else { 261 | t.Errorf(`got body = %v, wanted %v`, string(got), string(want)) 262 | } 263 | } 264 | } 265 | 266 | func TestJSONFormatterWriterError(t *testing.T) { 267 | // verifies if function doesn't panic if passed writer isn't *bytes.Buffer 268 | f := &JSONFormatter{} 269 | want := "underlying writer for JSONFormatter must be *bytes.Buffer" 270 | if err := f.Format(os.Stdout, []byte(`{}`)); err == nil || err.Error() != want { 271 | t.Errorf("got format error = %v, wanted %v", err, want) 272 | } 273 | } 274 | 275 | // newTransport creates a new HTTP Transport. 276 | // 277 | // BUG(henvic): this function is mostly used at this moment because of a data race condition on the standard library. 278 | // See https://github.com/golang/go/issues/30597 for details. 279 | func newTransport() *http.Transport { 280 | // values copied from Go 1.13.7 http.DefaultTransport variable. 281 | return &http.Transport{ 282 | Proxy: http.ProxyFromEnvironment, 283 | DialContext: (&net.Dialer{ 284 | Timeout: 30 * time.Second, 285 | KeepAlive: 30 * time.Second, 286 | DualStack: true, 287 | }).DialContext, 288 | ForceAttemptHTTP2: true, 289 | MaxIdleConns: 100, 290 | IdleConnTimeout: 90 * time.Second, 291 | TLSHandshakeTimeout: 10 * time.Second, 292 | ExpectContinueTimeout: 1 * time.Second, 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /internal/color/color.go: -------------------------------------------------------------------------------- 1 | // Package color can be used to add color to your terminal using ANSI escape code (or sequences). 2 | // 3 | // See https://en.wikipedia.org/wiki/ANSI_escape_code 4 | // Copy modified from https://github.com/fatih/color 5 | // Copyright 2013 Fatih Arslan 6 | package color 7 | 8 | import ( 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // Attribute defines a single SGR (Select Graphic Rendition) code. 15 | type Attribute int 16 | 17 | // Base attributes 18 | const ( 19 | Reset Attribute = iota 20 | Bold 21 | Faint 22 | Italic 23 | Underline 24 | BlinkSlow 25 | BlinkRapid 26 | ReverseVideo 27 | Concealed 28 | CrossedOut 29 | ) 30 | 31 | // Foreground text colors 32 | const ( 33 | FgBlack Attribute = iota + 30 34 | FgRed 35 | FgGreen 36 | FgYellow 37 | FgBlue 38 | FgMagenta 39 | FgCyan 40 | FgWhite 41 | ) 42 | 43 | // Foreground Hi-Intensity text colors 44 | const ( 45 | FgHiBlack Attribute = iota + 90 46 | FgHiRed 47 | FgHiGreen 48 | FgHiYellow 49 | FgHiBlue 50 | FgHiMagenta 51 | FgHiCyan 52 | FgHiWhite 53 | ) 54 | 55 | // Background text colors 56 | const ( 57 | BgBlack Attribute = iota + 40 58 | BgRed 59 | BgGreen 60 | BgYellow 61 | BgBlue 62 | BgMagenta 63 | BgCyan 64 | BgWhite 65 | ) 66 | 67 | // Background Hi-Intensity text colors 68 | const ( 69 | BgHiBlack Attribute = iota + 100 70 | BgHiRed 71 | BgHiGreen 72 | BgHiYellow 73 | BgHiBlue 74 | BgHiMagenta 75 | BgHiCyan 76 | BgHiWhite 77 | ) 78 | 79 | const ( 80 | escape = "\x1b" 81 | unescape = "\\x1b" 82 | ) 83 | 84 | // Format text for terminal. 85 | // You can pass an arbitrary number of Attribute or []Attribute followed by any other values, 86 | // that can either be a string or something else (that is converted to string using fmt.Sprint). 87 | func Format(s ...interface{}) string { 88 | if len(s) == 0 { 89 | return "" 90 | } 91 | 92 | params := []Attribute{} 93 | in := -1 94 | for i, v := range s { 95 | switch vt := v.(type) { 96 | case []Attribute: 97 | if in == -1 { 98 | params = append(params, vt...) 99 | } else { 100 | s[i] = printExtraColorAttribute(v) 101 | } 102 | case Attribute: 103 | if in == -1 { 104 | params = append(params, vt) 105 | } else { 106 | s[i] = printExtraColorAttribute(v) 107 | } 108 | default: 109 | if in == -1 { 110 | in = i 111 | } 112 | } 113 | } 114 | if in == -1 || len(s[in:]) == 0 { 115 | return "" 116 | } 117 | return wrap(params, fmt.Sprint(s[in:]...)) 118 | } 119 | 120 | func printExtraColorAttribute(v interface{}) string { 121 | return fmt.Sprintf("(EXTRA color.Attribute=%v)", v) 122 | } 123 | 124 | // StripAttributes from input arguments and return unformatted text. 125 | func StripAttributes(s ...interface{}) (raw string) { 126 | in := -1 127 | for i, v := range s { 128 | switch v.(type) { 129 | case []Attribute, Attribute: 130 | if in != -1 { 131 | s[i] = printExtraColorAttribute(v) 132 | } 133 | default: 134 | if in == -1 { 135 | in = i 136 | } 137 | } 138 | } 139 | if in == -1 { 140 | in = 0 141 | } 142 | return fmt.Sprint(s[in:]...) 143 | } 144 | 145 | // Escape text for terminal. 146 | func Escape(s string) string { 147 | return strings.Replace(s, escape, unescape, -1) 148 | } 149 | 150 | // sequence returns a formated SGR sequence to be plugged into a "\x1b[...m" 151 | // an example output might be: "1;36" -> bold cyan. 152 | func sequence(params []Attribute) string { 153 | format := make([]string, len(params)) 154 | for i, v := range params { 155 | format[i] = strconv.Itoa(int(v)) 156 | } 157 | 158 | return strings.Join(format, ";") 159 | } 160 | 161 | // wrap the s string with the colors attributes. 162 | func wrap(params []Attribute, s string) string { 163 | return fmt.Sprintf("%s[%sm%s%s[%dm", escape, sequence(params), s, escape, Reset) 164 | } 165 | -------------------------------------------------------------------------------- /internal/color/color_test.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestFormat(t *testing.T) { 9 | want := "\x1b[102;95mHello World\x1b[0m" 10 | got := Format(BgHiGreen, FgHiMagenta, "Hello World") 11 | if got != want { 12 | t.Errorf("Expecting %s, got '%s'\n", want, got) 13 | } 14 | } 15 | 16 | func TestMalformedFormat(t *testing.T) { 17 | want := "\x1b[102mHello World(EXTRA color.Attribute=95)\x1b[0m" 18 | got := Format(BgHiGreen, "Hello World", FgHiMagenta) 19 | if got != want { 20 | t.Errorf("Expecting %s, got '%s'\n", want, got) 21 | } 22 | } 23 | 24 | func TestMalformedSliceFormat(t *testing.T) { 25 | want := "\x1b[102mHello World(EXTRA color.Attribute=[95 41])\x1b[0m" 26 | got := Format(BgHiGreen, "Hello World", []Attribute{FgHiMagenta, BgRed}) 27 | if got != want { 28 | t.Errorf("Expecting %s, got '%s'\n", want, got) 29 | } 30 | } 31 | 32 | func TestFormatSlice(t *testing.T) { 33 | format := []Attribute{BgHiGreen, FgHiMagenta} 34 | want := "\x1b[102;95mHello World\x1b[0m" 35 | got := Format(format, "Hello World") 36 | if got != want { 37 | t.Errorf("Expecting %s, got '%s'\n", want, got) 38 | } 39 | } 40 | 41 | func TestEmpty(t *testing.T) { 42 | if want, got := "", Format(); got != want { 43 | t.Errorf("Expecting %s, got '%s'\n", want, got) 44 | } 45 | } 46 | 47 | func TestEmptyColorString(t *testing.T) { 48 | if want, got := "", Format(BgBlack); got != want { 49 | t.Errorf("Expecting %s, got '%s'\n", want, got) 50 | } 51 | } 52 | 53 | func TestNoFormat(t *testing.T) { 54 | want := "\x1b[mHello World\x1b[0m" 55 | got := Format("Hello World") 56 | if got != want { 57 | t.Errorf("Expecting %s, got '%s'\n", want, got) 58 | } 59 | } 60 | 61 | func TestFormatStartingWithNumber(t *testing.T) { 62 | want := "\x1b[102;95m100 forks\x1b[0m" 63 | number := 100 64 | if reflect.TypeOf(number).String() != "int" { 65 | t.Errorf("Must be integer; not a similar like Attribute") 66 | } 67 | if got := Format(BgHiGreen, FgHiMagenta, number, " forks"); got != want { 68 | t.Errorf("Expecting %s, got '%s'\n", want, got) 69 | } 70 | } 71 | 72 | func TestFormatCtrlChar(t *testing.T) { 73 | if want, got := "\x1b[ma%b\x1b[0m", Format("a%b"); got != want { 74 | t.Errorf(`expected Format(a%%b) to be %q, got %q instead`, want, got) 75 | } 76 | if want, got := "\\x1b[34;46ma%b\\x1b[0m", Escape(Format(FgBlue, BgCyan, "a%b")); got != want { 77 | t.Errorf(`expected escaped formatted a%%b to be %q, got %q instead`, want, got) 78 | } 79 | } 80 | 81 | func TestEscape(t *testing.T) { 82 | unescaped := "\x1b[32mGreen" 83 | escaped := "\\x1b[32mGreen" 84 | if got := Escape(unescaped); got != escaped { 85 | t.Errorf("Expecting %s, got '%s'\n", escaped, got) 86 | } 87 | } 88 | 89 | func TestStripAttributes(t *testing.T) { 90 | want := "this is a regular string" 91 | got := StripAttributes(FgCyan, []Attribute{FgBlack}, "this is a regular string") 92 | if got != want { 93 | t.Errorf("StripAttributes(input) = %s, wanted %s", got, want) 94 | } 95 | } 96 | 97 | func TestStripAttributesEmpty(t *testing.T) { 98 | if got := StripAttributes(); got != "" { 99 | t.Errorf("StripAttributes() should work") 100 | } 101 | } 102 | 103 | func TestStripAttributesFirstParam(t *testing.T) { 104 | want := "foo (EXTRA color.Attribute=32)" 105 | got := StripAttributes("foo ", FgGreen) 106 | if got != want { 107 | t.Errorf(`expected StripAttributes = %v, got %v instead`, want, got) 108 | } 109 | } 110 | 111 | func TestStripAttributesSame(t *testing.T) { 112 | want := "this is a regular string" 113 | got := StripAttributes(want) 114 | if got != want { 115 | t.Errorf("StripAttributes(%s) = %s, wanted %s", want, got, want) 116 | } 117 | } 118 | 119 | func TestStripAttributesWithExtraColorAttribute(t *testing.T) { 120 | want := "this is a regular string (EXTRA color.Attribute=91) with an invalid color Attribute field" 121 | got := StripAttributes(BgCyan, []Attribute{FgBlack}, "this is a regular string ", FgHiRed, " with an invalid color Attribute field") 122 | if got != want { 123 | t.Errorf("StripAttributes(input) = %s, wanted %s", got, want) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /internal/header/header.go: -------------------------------------------------------------------------------- 1 | // Package header can be used to sanitize HTTP request and response headers. 2 | package header 3 | 4 | import ( 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | // Sanitize list of headers. 11 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ can be consulted for header syntax. 12 | func Sanitize(sanitizers map[string]SanitizeHeaderFunc, headers http.Header) http.Header { 13 | var redacted = http.Header{} 14 | 15 | for k, values := range headers { 16 | if s, ok := sanitizers[http.CanonicalHeaderKey(k)]; ok { 17 | redacted[k] = sanitize(s, values) 18 | continue 19 | } 20 | 21 | redacted[k] = values 22 | } 23 | 24 | return redacted 25 | } 26 | 27 | func sanitize(s SanitizeHeaderFunc, values []string) []string { 28 | var redacted = []string{} 29 | 30 | for _, v := range values { 31 | redacted = append(redacted, s(v)) 32 | } 33 | 34 | return redacted 35 | } 36 | 37 | // DefaultSanitizers contains a list of sanitizers to be used for common headers. 38 | var DefaultSanitizers = map[string]SanitizeHeaderFunc{ 39 | "Authorization": AuthorizationSanitizer, 40 | "Set-Cookie": SetCookieSanitizer, 41 | "Cookie": CookieSanitizer, 42 | "Proxy-Authorization": AuthorizationSanitizer, 43 | } 44 | 45 | // SanitizeHeaderFunc implements sanitization for a header value. 46 | type SanitizeHeaderFunc func(string) string 47 | 48 | // AuthorizationSanitizer is used to sanitize Authorization and Proxy-Authorization headers. 49 | func AuthorizationSanitizer(unsafe string) string { 50 | if unsafe == "" { 51 | return "" 52 | } 53 | 54 | directives := strings.SplitN(unsafe, " ", 2) 55 | 56 | l := 0 57 | 58 | if len(directives) > 1 { 59 | l = len(directives[1]) 60 | } 61 | 62 | if l == 0 { 63 | return directives[0] 64 | } 65 | 66 | return directives[0] + " " + redact(l) 67 | } 68 | 69 | // SetCookieSanitizer is used to sanitize Set-Cookie header. 70 | func SetCookieSanitizer(unsafe string) string { 71 | directives := strings.SplitN(unsafe, ";", 2) 72 | 73 | cookie := strings.SplitN(directives[0], "=", 2) 74 | 75 | l := 0 76 | 77 | if len(cookie) > 1 { 78 | l = len(cookie[1]) 79 | } 80 | 81 | if len(directives) == 2 { 82 | return fmt.Sprintf("%s=%s; %s", cookie[0], redact(l), strings.TrimPrefix(directives[1], " ")) 83 | } 84 | 85 | return fmt.Sprintf("%s=%s", cookie[0], redact(l)) 86 | } 87 | 88 | // CookieSanitizer is used to sanitize Cookie header. 89 | func CookieSanitizer(unsafe string) string { 90 | cookies := strings.Split(unsafe, ";") 91 | 92 | var list []string 93 | 94 | for _, unsafeCookie := range cookies { 95 | cookie := strings.SplitN(unsafeCookie, "=", 2) 96 | l := 0 97 | 98 | if len(cookie) > 1 { 99 | l = len(cookie[1]) 100 | } 101 | 102 | list = append(list, fmt.Sprintf("%s=%s", cookie[0], redact(l))) 103 | } 104 | 105 | return strings.Join(list, "; ") 106 | } 107 | 108 | func redact(count int) string { 109 | if count == 0 { 110 | return "" 111 | } 112 | 113 | return "████████████████████" 114 | } 115 | -------------------------------------------------------------------------------- /internal/header/header_test.go: -------------------------------------------------------------------------------- 1 | package header 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSanitize(t *testing.T) { 10 | // no need to test request and response headers sanitization separately 11 | var headers = http.Header{} 12 | headers.Set("Accept", "*/*") 13 | headers.Set("User-Agent", "curl/7.54.0") 14 | headers.Add("Cookie", "abcd=secret1") 15 | headers.Add("Cookie", "xyz=secret2") 16 | headers.Add("Set-Cookie", "session_id=secret3") 17 | headers.Add("Set-Cookie", "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly") 18 | headers.Add("Authorization", "Bearer foo") 19 | headers.Add("Proxy-Authorization", "Basic Zm9vQGV4YW1wbGUuY29tOmJhcg==") 20 | headers.Set("Content-Type", "application/x-www-form-urlencoded") 21 | headers.Set("Content-Length", "3") 22 | 23 | var got = Sanitize(DefaultSanitizers, headers) 24 | if len(headers) != len(got) { 25 | t.Errorf("Expected length of sanitized headers (%d) to be equal to length of original headers (%d)", len(got), len(headers)) 26 | } 27 | want := http.Header{ 28 | "Accept": []string{"*/*"}, 29 | "User-Agent": []string{"curl/7.54.0"}, 30 | "Cookie": []string{"abcd=████████████████████", "xyz=████████████████████"}, 31 | "Set-Cookie": []string{"session_id=████████████████████", "id=████████████████████; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"}, 32 | "Authorization": []string{"Bearer ████████████████████"}, 33 | "Proxy-Authorization": []string{"Basic ████████████████████"}, 34 | "Content-Type": []string{"application/x-www-form-urlencoded"}, 35 | "Content-Length": []string{"3"}, 36 | } 37 | if !reflect.DeepEqual(got, want) { 38 | t.Errorf("Sanitized headers doesn't match expected value: wanted %+v, got %+v instead", want, got) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /printer.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "fmt" 8 | "io" 9 | "mime" 10 | "net" 11 | "net/http" 12 | "slices" 13 | "sort" 14 | "strings" 15 | "time" 16 | 17 | "github.com/henvic/httpretty/internal/color" 18 | "github.com/henvic/httpretty/internal/header" 19 | ) 20 | 21 | func newPrinter(l *Logger) printer { 22 | l.mu.Lock() 23 | defer l.mu.Unlock() 24 | return printer{ 25 | logger: l, 26 | flusher: l.flusher, 27 | } 28 | } 29 | 30 | type printer struct { 31 | flusher Flusher 32 | logger *Logger 33 | buf bytes.Buffer 34 | } 35 | 36 | func (p *printer) maybeOnReady() { 37 | if p.flusher == OnReady { 38 | p.flush() 39 | } 40 | } 41 | 42 | func (p *printer) flush() { 43 | if p.flusher == NoBuffer { 44 | return 45 | } 46 | p.logger.mu.Lock() 47 | defer p.logger.mu.Unlock() 48 | defer p.buf.Reset() 49 | w := p.logger.getWriter() 50 | fmt.Fprint(w, p.buf.String()) 51 | } 52 | 53 | func (p *printer) print(a ...interface{}) { 54 | p.logger.mu.Lock() 55 | defer p.logger.mu.Unlock() 56 | w := p.logger.getWriter() 57 | if p.flusher == NoBuffer { 58 | fmt.Fprint(w, a...) 59 | return 60 | } 61 | fmt.Fprint(&p.buf, a...) 62 | } 63 | 64 | func (p *printer) println(a ...interface{}) { 65 | p.logger.mu.Lock() 66 | defer p.logger.mu.Unlock() 67 | w := p.logger.getWriter() 68 | if p.flusher == NoBuffer { 69 | fmt.Fprintln(w, a...) 70 | return 71 | } 72 | fmt.Fprintln(&p.buf, a...) 73 | } 74 | 75 | func (p *printer) printf(format string, a ...interface{}) { 76 | p.logger.mu.Lock() 77 | defer p.logger.mu.Unlock() 78 | w := p.logger.getWriter() 79 | if p.flusher == NoBuffer { 80 | fmt.Fprintf(w, format, a...) 81 | return 82 | } 83 | fmt.Fprintf(&p.buf, format, a...) 84 | } 85 | 86 | func (p *printer) printRequest(req *http.Request) { 87 | if p.logger.RequestHeader { 88 | p.printRequestHeader(req) 89 | p.maybeOnReady() 90 | } 91 | if p.logger.RequestBody && req.Body != nil { 92 | p.printRequestBody(req) 93 | p.maybeOnReady() 94 | } 95 | } 96 | 97 | func (p *printer) printRequestInfo(req *http.Request) { 98 | to := req.URL.String() 99 | // req.URL.Host is empty on the request received by a server 100 | if req.URL.Host == "" { 101 | to = req.Host + to 102 | schema := "http://" 103 | if req.TLS != nil { 104 | schema = "https://" 105 | } 106 | to = schema + to 107 | } 108 | p.printf("* Request to %s\n", p.format(color.FgBlue, to)) 109 | if req.RemoteAddr != "" { 110 | p.printf("* Request from %s\n", p.format(color.FgBlue, req.RemoteAddr)) 111 | } 112 | } 113 | 114 | // checkFilter checkes if the request is filtered and if the Request value is nil. 115 | func (p *printer) checkFilter(req *http.Request) (skip bool) { 116 | filter := p.logger.getFilter() 117 | if req == nil { 118 | p.printf("> %s\n", p.format(color.FgRed, "error: null request")) 119 | return true 120 | } 121 | if filter == nil { 122 | return false 123 | } 124 | ok, err := safeFilter(filter, req) 125 | if err != nil { 126 | p.printf("* cannot filter request: %s: %s\n", p.format(color.FgBlue, fmt.Sprintf("%s %s", req.Method, req.URL)), p.format(color.FgRed, err.Error())) 127 | return false // never filter out the request if the filter errored 128 | } 129 | return ok 130 | } 131 | 132 | func safeFilter(filter Filter, req *http.Request) (skip bool, err error) { 133 | defer func() { 134 | if e := recover(); e != nil { 135 | err = fmt.Errorf("panic: %v", e) 136 | } 137 | }() 138 | return filter(req) 139 | } 140 | 141 | func (p *printer) printResponse(resp *http.Response) { 142 | if resp == nil { 143 | p.printf("< %s\n", p.format(color.FgRed, "error: null response")) 144 | p.maybeOnReady() 145 | return 146 | } 147 | if p.logger.ResponseHeader { 148 | p.printResponseHeader(resp.Proto, resp.Status, resp.Header) 149 | p.maybeOnReady() 150 | } 151 | if p.logger.ResponseBody && resp.Body != nil && (resp.Request == nil || resp.Request.Method != http.MethodHead) { 152 | p.printResponseBodyOut(resp) 153 | p.maybeOnReady() 154 | } 155 | } 156 | 157 | func (p *printer) checkBodyFiltered(h http.Header) (skip bool, err error) { 158 | if f := p.logger.getBodyFilter(); f != nil { 159 | defer func() { 160 | if e := recover(); e != nil { 161 | p.printf("* panic while filtering body: %v\n", e) 162 | } 163 | }() 164 | return f(h) 165 | } 166 | return false, nil 167 | } 168 | 169 | func (p *printer) printResponseBodyOut(resp *http.Response) { 170 | if resp.ContentLength == 0 { 171 | return 172 | } 173 | skip, err := p.checkBodyFiltered(resp.Header) 174 | if err != nil { 175 | p.printf("* %s\n", p.format(color.FgRed, "error on response body filter: ", err.Error())) 176 | } 177 | if skip { 178 | return 179 | } 180 | if contentType := resp.Header.Get("Content-Type"); contentType != "" && isBinaryMediatype(contentType) { 181 | p.println("* body contains binary data") 182 | return 183 | } 184 | if p.logger.MaxResponseBody > 0 && resp.ContentLength > p.logger.MaxResponseBody { 185 | p.printf("* body is too long (%d bytes) to print, skipping (longer than %d bytes)\n", resp.ContentLength, p.logger.MaxResponseBody) 186 | return 187 | } 188 | contentType := resp.Header.Get("Content-Type") 189 | if resp.ContentLength == -1 { 190 | if newBody := p.printBodyUnknownLength(contentType, p.logger.MaxResponseBody, resp.Body); newBody != nil { 191 | resp.Body = newBody 192 | } 193 | return 194 | } 195 | var buf bytes.Buffer 196 | tee := io.TeeReader(resp.Body, &buf) 197 | defer resp.Body.Close() 198 | defer func() { 199 | resp.Body = io.NopCloser(&buf) 200 | }() 201 | p.printBodyReader(contentType, tee) 202 | } 203 | 204 | // isBinary uses heuristics to guess if file is binary (actually, "printable" in the terminal). 205 | // See discussion at https://groups.google.com/forum/#!topic/golang-nuts/YeLL7L7SwWs 206 | func isBinary(body []byte) bool { 207 | if len(body) > 512 { 208 | body = body[512:] 209 | } 210 | // If file contains UTF-8 OR UTF-16 BOM, consider it non-binary. 211 | // Reference: https://tools.ietf.org/html/draft-ietf-websec-mime-sniff-03#section-5 212 | if len(body) >= 3 && (bytes.Equal(body[:2], []byte{0xFE, 0xFF}) || // UTF-16BE BOM 213 | bytes.Equal(body[:2], []byte{0xFF, 0xFE}) || // UTF-16LE BOM 214 | bytes.Equal(body[:3], []byte{0xEF, 0xBB, 0xBF})) { // UTF-8 BOM 215 | return false 216 | } 217 | // If all of the first n octets are binary data octets, consider it binary. 218 | // Reference: https://github.com/golang/go/blob/349e7df2c3d0f9b5429e7c86121499c137faac7e/src/net/http/sniff.go#L297-L309 219 | // c.f. section 5, step 4. 220 | for _, b := range body { 221 | switch { 222 | case b <= 0x08, 223 | b == 0x0B, 224 | 0x0E <= b && b <= 0x1A, 225 | 0x1C <= b && b <= 0x1F: 226 | return true 227 | } 228 | } 229 | // Otherwise, check against a white list of binary mimetypes. 230 | mediatype, _, err := mime.ParseMediaType(http.DetectContentType(body)) 231 | if err != nil { 232 | return false 233 | } 234 | return isBinaryMediatype(mediatype) 235 | } 236 | 237 | var binaryMediatypes = map[string]struct{}{ 238 | "application/pdf": {}, 239 | "application/postscript": {}, 240 | "image": {}, // for practical reasons, any image (including SVG) is considered binary data 241 | "audio": {}, 242 | "application/ogg": {}, 243 | "video": {}, 244 | "application/vnd.ms-fontobject": {}, 245 | "font": {}, 246 | "application/x-gzip": {}, 247 | "application/zip": {}, 248 | "application/x-rar-compressed": {}, 249 | "application/wasm": {}, 250 | } 251 | 252 | func isBinaryMediatype(mediatype string) bool { 253 | if _, ok := binaryMediatypes[mediatype]; ok { 254 | return true 255 | } 256 | if parts := strings.SplitN(mediatype, "/", 2); len(parts) == 2 { 257 | if _, ok := binaryMediatypes[parts[0]]; ok { 258 | return true 259 | } 260 | } 261 | return false 262 | } 263 | 264 | const maxDefaultUnknownReadable = 4096 // bytes 265 | 266 | func (p *printer) printBodyUnknownLength(contentType string, maxLength int64, r io.ReadCloser) (newBody io.ReadCloser) { 267 | if maxLength == 0 { 268 | maxLength = maxDefaultUnknownReadable 269 | } 270 | pb := make([]byte, maxLength+1) // read one extra bit to assure the length is longer than acceptable 271 | n, err := io.ReadFull(r, pb) 272 | pb = pb[0:n] // trim any nil symbols left after writing in the byte slice. 273 | buf := bytes.NewReader(pb) 274 | newBody = newBodyReaderBuf(buf, r) 275 | switch { 276 | // Server requests always return req.Body != nil, but the Reader returns io.EOF immediately. 277 | // Avoiding returning early to mitigate any risk of bad reader implementations that might 278 | // send something even after returning io.EOF if read again. 279 | case err == io.EOF && n == 0: 280 | case err == nil && int64(n) > maxLength: 281 | p.printf("* body is too long, skipping (contains more than %d bytes)\n", n-1) 282 | case err == io.ErrUnexpectedEOF || err == nil: 283 | // cannot pass same bytes reader below because we only read it once. 284 | p.printBodyReader(contentType, bytes.NewReader(pb)) 285 | default: 286 | p.printf("* cannot read body: %v (%d bytes read)\n", err, n) 287 | } 288 | return 289 | } 290 | 291 | func findPeerCertificate(hostname string, state *tls.ConnectionState) (cert *x509.Certificate) { 292 | if chains := state.VerifiedChains; chains != nil && chains[0] != nil && chains[0][0] != nil { 293 | return chains[0][0] 294 | } 295 | if hostname == "" && len(state.PeerCertificates) > 0 { 296 | // skip finding a match for a given hostname if hostname is not available (e.g., a client certificate) 297 | return state.PeerCertificates[0] 298 | } 299 | // the chain is not created when tls.Config.InsecureSkipVerify is set, then let's try to find a match to display 300 | for _, cert := range state.PeerCertificates { 301 | if err := cert.VerifyHostname(hostname); err == nil { 302 | return cert 303 | } 304 | } 305 | return nil 306 | } 307 | 308 | func (p *printer) printTLSInfo(state *tls.ConnectionState, skipVerifyChains bool) { 309 | if state == nil { 310 | return 311 | } 312 | protocol := tlsProtocolVersions[state.Version] 313 | if protocol == "" { 314 | protocol = fmt.Sprintf("%#v", state.Version) 315 | } 316 | cipher := tlsCiphers[state.CipherSuite] 317 | if cipher == "" { 318 | cipher = fmt.Sprintf("%#v", state.CipherSuite) 319 | } 320 | p.printf("* TLS connection using %s / %s", p.format(color.FgBlue, protocol), p.format(color.FgBlue, cipher)) 321 | if !skipVerifyChains && state.VerifiedChains == nil { 322 | p.print(" (insecure=true)") 323 | } 324 | p.println() 325 | if state.NegotiatedProtocol != "" { 326 | p.printf("* ALPN: %v accepted\n", p.format(color.FgBlue, state.NegotiatedProtocol)) 327 | } 328 | } 329 | 330 | func (p *printer) printOutgoingClientTLS(config *tls.Config) { 331 | if config == nil || len(config.Certificates) == 0 { 332 | return 333 | } 334 | p.println("* Client certificate:") 335 | // Please notice tls.Config.BuildNameToCertificate() doesn't store the certificate Leaf field. 336 | // You need to explicitly parse and store it with something such as: 337 | // cert.Leaf, err = x509.ParseCertificate(cert.Certificate) 338 | if cert := config.Certificates[0].Leaf; cert != nil { 339 | p.printCertificate("", cert) 340 | } else { 341 | p.println(`** unparsed certificate found, skipping`) 342 | } 343 | } 344 | 345 | func (p *printer) printIncomingClientTLS(state *tls.ConnectionState) { 346 | // if no TLS state is null or no client TLS certificate is found, return early. 347 | if state == nil || len(state.PeerCertificates) == 0 { 348 | return 349 | } 350 | p.println("* Client certificate:") 351 | if cert := findPeerCertificate("", state); cert != nil { 352 | p.printCertificate("", cert) 353 | } else { 354 | p.println(p.format(color.FgRed, "** No valid certificate was found")) 355 | } 356 | } 357 | 358 | func (p *printer) printTLSServer(host string, state *tls.ConnectionState) { 359 | if state == nil { 360 | return 361 | } 362 | hostname, _, err := net.SplitHostPort(host) 363 | if err != nil { 364 | // assume the error is due to "missing port in address" 365 | hostname = host 366 | } 367 | p.println("* Server certificate:") 368 | if cert := findPeerCertificate(hostname, state); cert != nil { 369 | // server certificate messages are slightly similar to how "curl -v" shows 370 | p.printCertificate(hostname, cert) 371 | } else { 372 | p.println(p.format(color.FgRed, "** No valid certificate was found")) 373 | } 374 | } 375 | 376 | func (p *printer) printCertificate(hostname string, cert *x509.Certificate) { 377 | p.printf(`* subject: %v 378 | * start date: %v 379 | * expire date: %v 380 | * issuer: %v 381 | `, 382 | p.format(color.FgBlue, cert.Subject), 383 | p.format(color.FgBlue, cert.NotBefore.Format(time.UnixDate)), 384 | p.format(color.FgBlue, cert.NotAfter.Format(time.UnixDate)), 385 | p.format(color.FgBlue, cert.Issuer), 386 | ) 387 | if hostname == "" { 388 | return 389 | } 390 | if err := cert.VerifyHostname(hostname); err != nil { 391 | p.printf("* %s\n", p.format(color.FgRed, err.Error())) 392 | return 393 | } 394 | p.println("* TLS certificate verify ok.") 395 | } 396 | 397 | func (p *printer) printServerResponse(req *http.Request, rec *responseRecorder) { 398 | if p.logger.ResponseHeader { 399 | // TODO(henvic): see how httptest.ResponseRecorder adds extra headers due to Content-Type detection 400 | // and other stuff (Date). It would be interesting to show them here too (either as default or opt-in). 401 | p.printResponseHeader(req.Proto, fmt.Sprintf("%d %s", rec.statusCode, http.StatusText(rec.statusCode)), rec.Header()) 402 | } 403 | if !p.logger.ResponseBody || rec.size == 0 { 404 | return 405 | } 406 | skip, err := p.checkBodyFiltered(rec.Header()) 407 | if err != nil { 408 | p.printf("* %s\n", p.format(color.FgRed, "error on response body filter: ", err.Error())) 409 | } 410 | if skip { 411 | return 412 | } 413 | if mediatype := req.Header.Get("Content-Type"); mediatype != "" && isBinaryMediatype(mediatype) { 414 | p.println("* body contains binary data") 415 | return 416 | } 417 | if p.logger.MaxResponseBody > 0 && rec.size > p.logger.MaxResponseBody { 418 | p.printf("* body is too long (%d bytes) to print, skipping (longer than %d bytes)\n", rec.size, p.logger.MaxResponseBody) 419 | return 420 | } 421 | p.printBodyReader(rec.Header().Get("Content-Type"), rec.buf) 422 | } 423 | 424 | func (p *printer) printResponseHeader(proto, status string, h http.Header) { 425 | p.printf("< %s %s\n", 426 | p.format(color.FgBlue, color.Bold, proto), 427 | p.format(color.FgRed, status)) 428 | p.printHeaders('<', h) 429 | p.println() 430 | } 431 | 432 | func (p *printer) printBodyReader(contentType string, r io.Reader) { 433 | mediatype, _, _ := mime.ParseMediaType(contentType) 434 | body, err := io.ReadAll(r) 435 | if err != nil { 436 | p.printf("* cannot read body: %v\n", p.format(color.FgRed, err.Error())) 437 | return 438 | } 439 | if isBinary(body) { 440 | p.println("* body contains binary data") 441 | return 442 | } 443 | for _, f := range p.logger.Formatters { 444 | if ok := p.safeBodyMatch(f, mediatype); !ok { 445 | continue 446 | } 447 | var formatted bytes.Buffer 448 | switch err := p.safeBodyFormat(f, &formatted, body); { 449 | case err != nil: 450 | p.printf("* body cannot be formatted: %v\n%s\n", p.format(color.FgRed, err.Error()), string(body)) 451 | default: 452 | p.println(formatted.String()) 453 | } 454 | return 455 | } 456 | 457 | p.println(string(body)) 458 | } 459 | 460 | func (p *printer) safeBodyMatch(f Formatter, mediatype string) bool { 461 | defer func() { 462 | if e := recover(); e != nil { 463 | p.printf("* panic while testing body format: %v\n", e) 464 | } 465 | }() 466 | return f.Match(mediatype) 467 | } 468 | 469 | func (p *printer) safeBodyFormat(f Formatter, w io.Writer, src []byte) (err error) { 470 | defer func() { 471 | // should not return panic as error because we want to try the next formatter 472 | if e := recover(); e != nil { 473 | err = fmt.Errorf("panic: %v", e) 474 | } 475 | }() 476 | return f.Format(w, src) 477 | } 478 | 479 | func (p *printer) format(s ...interface{}) string { 480 | if p.logger.Colors { 481 | return color.Format(s...) 482 | } 483 | return color.StripAttributes(s...) 484 | } 485 | 486 | func (p *printer) printHeaders(prefix rune, h http.Header) { 487 | if !p.logger.SkipSanitize { 488 | h = header.Sanitize(header.DefaultSanitizers, h) 489 | } 490 | 491 | longest, sorted := sortHeaderKeys(h, p.logger.cloneSkipHeader()) 492 | for _, key := range sorted { 493 | for _, v := range h[key] { 494 | var pad string 495 | if p.logger.Align { 496 | pad = strings.Repeat(" ", longest-len(key)) 497 | } 498 | p.printf("%c %s%s %s%s\n", prefix, 499 | p.format(color.FgBlue, color.Bold, key), 500 | p.format(color.FgRed, ":"), 501 | pad, 502 | p.format(color.FgYellow, v)) 503 | } 504 | } 505 | } 506 | 507 | func sortHeaderKeys(h http.Header, skipped map[string]struct{}) (int, []string) { 508 | var ( 509 | keys = make([]string, 0, len(h)) 510 | longest int 511 | ) 512 | for key := range h { 513 | if _, skip := skipped[key]; skip { 514 | continue 515 | } 516 | keys = append(keys, key) 517 | if l := len(key); l > longest { 518 | longest = l 519 | } 520 | } 521 | sort.Strings(keys) 522 | if i := slices.Index(keys, "Host"); i > -1 { 523 | keys = append([]string{"Host"}, slices.Delete(keys, i, i+1)...) 524 | } 525 | return longest, keys 526 | } 527 | 528 | func (p *printer) printRequestHeader(req *http.Request) { 529 | p.printf("> %s %s %s\n", 530 | p.format(color.FgBlue, color.Bold, req.Method), 531 | p.format(color.FgYellow, req.URL.RequestURI()), 532 | p.format(color.FgBlue, req.Proto)) 533 | p.printHeaders('>', addRequestHeaders(req)) 534 | p.println() 535 | } 536 | 537 | // addRequestHeaders returns a copy of the given header with an additional headers set, if known. 538 | func addRequestHeaders(req *http.Request) http.Header { 539 | cp := http.Header{} 540 | for k, v := range req.Header { 541 | cp[k] = v 542 | } 543 | 544 | if len(req.Header.Values("Content-Length")) == 0 && req.ContentLength > 0 { 545 | cp.Set("Content-Length", fmt.Sprintf("%d", req.ContentLength)) 546 | } 547 | 548 | host := req.Host 549 | if host == "" { 550 | host = req.URL.Host 551 | } 552 | if host != "" { 553 | cp.Set("Host", host) 554 | } 555 | return cp 556 | } 557 | 558 | func (p *printer) printRequestBody(req *http.Request) { 559 | // For client requests, a request with zero content-length and no body is also treated as unknown. 560 | if req.Body == nil { 561 | return 562 | } 563 | skip, err := p.checkBodyFiltered(req.Header) 564 | if err != nil { 565 | p.printf("* %s\n", p.format(color.FgRed, "error on request body filter: ", err.Error())) 566 | } 567 | if skip { 568 | return 569 | } 570 | if mediatype := req.Header.Get("Content-Type"); mediatype != "" && isBinaryMediatype(mediatype) { 571 | p.println("* body contains binary data") 572 | return 573 | } 574 | // TODO(henvic): add support for printing multipart/formdata information as body (to responses too). 575 | if p.logger.MaxRequestBody > 0 && req.ContentLength > p.logger.MaxRequestBody { 576 | p.printf("* body is too long (%d bytes) to print, skipping (longer than %d bytes)\n", 577 | req.ContentLength, p.logger.MaxRequestBody) 578 | return 579 | } 580 | contentType := req.Header.Get("Content-Type") 581 | if req.ContentLength > 0 { 582 | var buf bytes.Buffer 583 | tee := io.TeeReader(req.Body, &buf) 584 | defer req.Body.Close() 585 | defer func() { 586 | req.Body = io.NopCloser(&buf) 587 | }() 588 | p.printBodyReader(contentType, tee) 589 | return 590 | } 591 | if newBody := p.printBodyUnknownLength(contentType, p.logger.MaxRequestBody, req.Body); newBody != nil { 592 | req.Body = newBody 593 | } 594 | } 595 | 596 | func (p *printer) printTimeRequest() (end func()) { 597 | startRequest := time.Now() 598 | p.printf("* Request at %v\n", startRequest) 599 | return func() { 600 | p.printf("* Request took %v\n", time.Since(startRequest)) 601 | } 602 | } 603 | -------------------------------------------------------------------------------- /race_test.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | // +build race 3 | 4 | package httpretty 5 | 6 | func init() { 7 | race = true 8 | } 9 | -------------------------------------------------------------------------------- /recorder.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type bodyCloser struct { 10 | r io.Reader 11 | close func() error 12 | } 13 | 14 | func (bc *bodyCloser) Read(p []byte) (n int, err error) { 15 | return bc.r.Read(p) 16 | } 17 | 18 | func (bc *bodyCloser) Close() error { 19 | return bc.close() 20 | } 21 | 22 | func newBodyReaderBuf(buf io.Reader, body io.ReadCloser) *bodyCloser { 23 | return &bodyCloser{ 24 | r: io.MultiReader(buf, body), 25 | close: body.Close, 26 | } 27 | } 28 | 29 | type responseRecorder struct { 30 | http.ResponseWriter 31 | statusCode int 32 | maxReadableBody int64 33 | size int64 34 | buf *bytes.Buffer 35 | } 36 | 37 | // Write the data to the connection as part of an HTTP reply, and records it. 38 | func (rr *responseRecorder) Write(p []byte) (int, error) { 39 | rr.size += int64(len(p)) 40 | if rr.maxReadableBody > 0 && rr.size > rr.maxReadableBody { 41 | rr.buf = nil 42 | return rr.ResponseWriter.Write(p) 43 | } 44 | defer rr.buf.Write(p) 45 | return rr.ResponseWriter.Write(p) 46 | } 47 | 48 | // WriteHeader sends an HTTP response header with the provided 49 | // status code, and records it. 50 | func (rr *responseRecorder) WriteHeader(statusCode int) { 51 | rr.ResponseWriter.WriteHeader(statusCode) 52 | rr.statusCode = statusCode 53 | } 54 | -------------------------------------------------------------------------------- /scripts/ci-lint-fmt.sh: -------------------------------------------------------------------------------- 1 | # Adapted from @aminueza's go-github-action/fmt/fmt.sh 2 | # Reference: https://github.com/aminueza/go-github-action/blob/master/fmt/fmt.sh 3 | # Execute fmt tool, resolve and emit each unformatted file 4 | UNFORMATTED_FILES=$(go fmt $(go list ./... | grep -v /vendor/)) 5 | 6 | if [ -n "$UNFORMATTED_FILES" ]; then 7 | echo '::error::The following files are not properly formatted:' 8 | echo "$UNFORMATTED_FILES" | while read -r LINE; do 9 | FILE=$(realpath --relative-base="." "$LINE") 10 | echo "::error:: $FILE" 11 | done 12 | exit 1 13 | fi 14 | -------------------------------------------------------------------------------- /scripts/ci-lint-install.sh: -------------------------------------------------------------------------------- 1 | source scripts/lib.sh 2 | 3 | ensure_go_binary honnef.co/go/tools/cmd/staticcheck 4 | ensure_go_binary github.com/securego/gosec/v2/cmd/gosec 5 | -------------------------------------------------------------------------------- /scripts/ci-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | # Static analysis scripts 6 | cd $(dirname $0)/.. 7 | 8 | source scripts/ci-lint-install.sh 9 | source scripts/ci-lint-fmt.sh 10 | 11 | set -x 12 | go vet ./... 13 | staticcheck ./... 14 | # Exclude rule G114 due to example/server using it as a demo. 15 | gosec -quiet -exclude G114 ./... 16 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Modified version of chef-runner/script/coverage 4 | # Copyright 2004 Mathias Lafeldt 5 | # Apache License 2.0 6 | # Source: https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage 7 | 8 | # Generate test coverage statistics for Go packages. 9 | # 10 | # Works around the fact that `go test -coverprofile` currently does not work 11 | # with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909 12 | # 13 | # Usage: script/coverage [--html|--coveralls] 14 | # 15 | # --html Additionally create HTML report and open it in browser 16 | # --coveralls Push coverage statistics to coveralls.io 17 | # 18 | # Changes: directories ending in .go used to fail 19 | 20 | set -e 21 | 22 | workdir=.cover 23 | profile="$workdir/cover.out" 24 | mode=count 25 | 26 | generate_cover_data() { 27 | rm -rf "$workdir" 28 | mkdir "$workdir" 29 | 30 | for pkg in "$@"; do 31 | f="$workdir/$(echo $pkg | tr / -).cover" 32 | go test -covermode="$mode" -coverprofile="$f" "$pkg/" 33 | done 34 | 35 | echo "mode: $mode" >"$profile" 36 | grep -h -v "^mode:" "$workdir"/*.cover >>"$profile" 37 | } 38 | 39 | show_cover_report() { 40 | go tool cover -${1}="$profile" 41 | } 42 | 43 | push_to_coveralls() { 44 | echo "Pushing coverage statistics to coveralls.io" 45 | goveralls -coverprofile="$profile" 46 | } 47 | 48 | generate_cover_data $(go list ./...) 49 | show_cover_report func 50 | case "$1" in 51 | "") 52 | ;; 53 | --html) 54 | show_cover_report html ;; 55 | --coveralls) 56 | push_to_coveralls ;; 57 | *) 58 | echo >&2 "error: invalid option: $1"; exit 1 ;; 59 | esac 60 | -------------------------------------------------------------------------------- /scripts/lib.sh: -------------------------------------------------------------------------------- 1 | # ensure_go_binary verifies that a binary exists in $PATH corresponding to the 2 | # given go-gettable URI. If no such binary exists, it is fetched via `go get`. 3 | # Reference: https://github.com/golang/pkgsite/blob/65d33554b34b666d37b22bed7de136b562d5dba8/all.bash#L93-L103 4 | # Copyright 2019 The Go Authors. 5 | ensure_go_binary() { 6 | local binary=$(basename $1) 7 | if ! [ -x "$(command -v $binary)" ]; then 8 | echo "Installing: $1" 9 | # Run in a subshell for convenience, so that we don't have to worry about 10 | # our PWD. 11 | (set -x; cd && go install $1@latest) 12 | fi 13 | } 14 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "errors" 9 | "fmt" 10 | "mime" 11 | "mime/multipart" 12 | "net" 13 | "net/http" 14 | "net/http/httptest" 15 | "net/url" 16 | "os" 17 | "regexp" 18 | "strings" 19 | "sync" 20 | "testing" 21 | "time" 22 | ) 23 | 24 | // inspect a request (not concurrency safe). 25 | func inspect(next http.Handler, wait int) *inspectHandler { 26 | is := &inspectHandler{ 27 | next: next, 28 | } 29 | is.wg.Add(wait) 30 | return is 31 | } 32 | 33 | type inspectHandler struct { 34 | next http.Handler 35 | wg sync.WaitGroup 36 | req *http.Request 37 | } 38 | 39 | func (h *inspectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 40 | h.req = req 41 | h.next.ServeHTTP(w, req) 42 | h.wg.Done() 43 | } 44 | 45 | func (h *inspectHandler) Wait() { 46 | h.wg.Wait() 47 | } 48 | 49 | func TestIncoming(t *testing.T) { 50 | t.Parallel() 51 | logger := &Logger{ 52 | RequestHeader: true, 53 | RequestBody: true, 54 | ResponseHeader: true, 55 | ResponseBody: true, 56 | } 57 | var buf bytes.Buffer 58 | logger.SetOutput(&buf) 59 | is := inspect(logger.Middleware(helloHandler{}), 1) 60 | ts := httptest.NewServer(is) 61 | defer ts.Close() 62 | 63 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 64 | if err != nil { 65 | t.Errorf("cannot create request: %v", err) 66 | } 67 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 68 | go func() { 69 | client := newServerClient() 70 | resp, err := client.Do(req) 71 | if err != nil { 72 | t.Errorf("cannot connect to the server: %v", err) 73 | } 74 | testBody(t, resp.Body, []byte("Hello, world!")) 75 | }() 76 | 77 | is.Wait() 78 | want := fmt.Sprintf(golden(t.Name()), is.req.Host, is.req.RemoteAddr, ts.Listener.Addr()) 79 | if got := buf.String(); got != want { 80 | t.Errorf("logged HTTP request %s; want %s", got, want) 81 | } 82 | } 83 | 84 | func TestIncomingNotFound(t *testing.T) { 85 | t.Parallel() 86 | logger := &Logger{ 87 | RequestHeader: true, 88 | ResponseHeader: true, 89 | } 90 | var buf bytes.Buffer 91 | logger.SetOutput(&buf) 92 | is := inspect(logger.Middleware(http.NotFoundHandler()), 1) 93 | ts := httptest.NewServer(is) 94 | defer ts.Close() 95 | 96 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 97 | if err != nil { 98 | t.Errorf("cannot create request: %v", err) 99 | } 100 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 101 | go func() { 102 | client := newServerClient() 103 | resp, err := client.Do(req) 104 | if err != nil { 105 | t.Errorf("cannot connect to the server: %v", err) 106 | } 107 | if resp.StatusCode != http.StatusNotFound { 108 | t.Errorf("got status codem %v, wanted %v", resp.StatusCode, http.StatusNotFound) 109 | } 110 | }() 111 | is.Wait() 112 | want := fmt.Sprintf(golden(t.Name()), is.req.Host, is.req.RemoteAddr, ts.Listener.Addr()) 113 | if got := buf.String(); got != want { 114 | t.Errorf("logged HTTP request %s; want %s", got, want) 115 | } 116 | } 117 | 118 | func outgoingGetServer(client *http.Client, ts *httptest.Server, done func()) { 119 | defer done() 120 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 121 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 122 | if err != nil { 123 | panic(err) 124 | } 125 | if _, err := client.Do(req); err != nil { 126 | panic(err) 127 | } 128 | } 129 | 130 | func TestIncomingConcurrency(t *testing.T) { 131 | logger := &Logger{ 132 | TLS: true, 133 | RequestHeader: true, 134 | RequestBody: true, 135 | ResponseHeader: true, 136 | ResponseBody: true, 137 | } 138 | logger.SetFlusher(OnEnd) 139 | var buf bytes.Buffer 140 | logger.SetOutput(&buf) 141 | ts := httptest.NewServer(logger.Middleware(helloHandler{})) 142 | defer ts.Close() 143 | 144 | concurrency := 100 145 | { 146 | var wg sync.WaitGroup 147 | wg.Add(concurrency) 148 | i := 0 149 | repeat: 150 | client := &http.Client{ 151 | Transport: newTransport(), 152 | } 153 | go outgoingGetServer(client, ts, wg.Done) 154 | if i < concurrency-1 { 155 | i++ 156 | time.Sleep(2 * time.Millisecond) 157 | goto repeat 158 | } 159 | wg.Wait() 160 | } 161 | 162 | got := buf.String() 163 | gotConcurrency := strings.Count(got, "< HTTP/1.1 200 OK") 164 | if concurrency != gotConcurrency { 165 | t.Errorf("logged %d requests, wanted %d", concurrency, gotConcurrency) 166 | } 167 | want := fmt.Sprintf(golden(t.Name()), ts.Listener.Addr()) 168 | if !strings.Contains(got, want) { 169 | t.Errorf("Request doesn't contain expected body") 170 | } 171 | } 172 | 173 | func TestIncomingMinimal(t *testing.T) { 174 | t.Parallel() 175 | // only prints the request URI and remote address that requested it. 176 | logger := &Logger{} 177 | var buf bytes.Buffer 178 | logger.SetOutput(&buf) 179 | is := inspect(logger.Middleware(helloHandler{}), 1) 180 | 181 | ts := httptest.NewServer(is) 182 | defer ts.Close() 183 | uri := fmt.Sprintf("%s/incoming", ts.URL) 184 | go func() { 185 | client := newServerClient() 186 | req, err := http.NewRequest(http.MethodGet, uri, nil) 187 | if err != nil { 188 | t.Errorf("cannot create request: %v", err) 189 | } 190 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 191 | req.AddCookie(&http.Cookie{ 192 | Name: "food", 193 | Value: "sorbet", 194 | }) 195 | if _, err = client.Do(req); err != nil { 196 | t.Errorf("cannot connect to the server: %v", err) 197 | } 198 | }() 199 | is.Wait() 200 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr) 201 | if got := buf.String(); got != want { 202 | t.Errorf("logged HTTP request %s; want %s", got, want) 203 | } 204 | } 205 | 206 | func TestIncomingSanitized(t *testing.T) { 207 | t.Parallel() 208 | logger := &Logger{ 209 | RequestHeader: true, 210 | RequestBody: true, 211 | ResponseHeader: true, 212 | ResponseBody: true, 213 | } 214 | var buf bytes.Buffer 215 | logger.SetOutput(&buf) 216 | is := inspect(logger.Middleware(helloHandler{}), 1) 217 | 218 | ts := httptest.NewServer(is) 219 | defer ts.Close() 220 | uri := fmt.Sprintf("%s/incoming", ts.URL) 221 | go func() { 222 | client := newServerClient() 223 | req, err := http.NewRequest(http.MethodGet, uri, nil) 224 | if err != nil { 225 | t.Errorf("cannot create request: %v", err) 226 | } 227 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 228 | req.AddCookie(&http.Cookie{ 229 | Name: "food", 230 | Value: "sorbet", 231 | }) 232 | 233 | if _, err = client.Do(req); err != nil { 234 | t.Errorf("cannot connect to the server: %v", err) 235 | } 236 | }() 237 | is.Wait() 238 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 239 | if got := buf.String(); got != want { 240 | t.Errorf("logged HTTP request %s; want %s", got, want) 241 | } 242 | } 243 | 244 | type hideHandler struct { 245 | next http.Handler 246 | } 247 | 248 | func (h hideHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 249 | req = req.WithContext(WithHide(context.Background())) 250 | h.next.ServeHTTP(w, req) 251 | } 252 | 253 | func TestIncomingHide(t *testing.T) { 254 | t.Parallel() 255 | logger := &Logger{ 256 | RequestHeader: true, 257 | RequestBody: true, 258 | ResponseHeader: true, 259 | ResponseBody: true, 260 | } 261 | var buf bytes.Buffer 262 | logger.SetOutput(&buf) 263 | is := inspect(hideHandler{ 264 | next: logger.Middleware(helloHandler{}), 265 | }, 1) 266 | ts := httptest.NewServer(is) 267 | defer ts.Close() 268 | go func() { 269 | client := newServerClient() 270 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 271 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 272 | if err != nil { 273 | t.Errorf("cannot create request: %v", err) 274 | } 275 | if _, err = client.Do(req); err != nil { 276 | t.Errorf("cannot connect to the server: %v", err) 277 | } 278 | }() 279 | is.Wait() 280 | if buf.Len() != 0 { 281 | t.Errorf("request should not be logged, got %v", buf.String()) 282 | } 283 | } 284 | 285 | func TestIncomingFilter(t *testing.T) { 286 | t.Parallel() 287 | logger := &Logger{ 288 | RequestHeader: true, 289 | RequestBody: true, 290 | ResponseHeader: true, 291 | ResponseBody: true, 292 | } 293 | var buf bytes.Buffer 294 | logger.SetOutput(&buf) 295 | logger.SetFilter(filteredURIs) 296 | ts := httptest.NewServer(logger.Middleware(helloHandler{})) 297 | defer ts.Close() 298 | testCases := []struct { 299 | uri string 300 | want string 301 | }{ 302 | {uri: "filtered"}, 303 | {uri: "unfiltered", want: "* Request"}, 304 | {uri: "other", want: "filter error triggered"}, 305 | } 306 | for _, tc := range testCases { 307 | t.Run(tc.uri, func(t *testing.T) { 308 | var buf bytes.Buffer 309 | logger.SetOutput(&buf) 310 | client := newServerClient() 311 | _, err := client.Get(fmt.Sprintf("%s/%s", ts.URL, tc.uri)) 312 | if err != nil { 313 | t.Errorf("cannot create request: %v", err) 314 | } 315 | if tc.want == "" && buf.Len() != 0 { 316 | t.Errorf("wanted input to be filtered, got %v instead", buf.String()) 317 | } 318 | if !strings.Contains(buf.String(), tc.want) { 319 | t.Errorf(`expected input to contain "%v", got %v instead`, tc.want, buf.String()) 320 | } 321 | }) 322 | } 323 | } 324 | 325 | func TestIncomingFilterPanicked(t *testing.T) { 326 | t.Parallel() 327 | logger := &Logger{ 328 | RequestHeader: true, 329 | RequestBody: true, 330 | ResponseHeader: true, 331 | ResponseBody: true, 332 | } 333 | var buf bytes.Buffer 334 | logger.SetOutput(&buf) 335 | logger.SetFilter(func(req *http.Request) (bool, error) { 336 | panic("evil panic") 337 | }) 338 | is := inspect(logger.Middleware(helloHandler{}), 1) 339 | ts := httptest.NewServer(is) 340 | defer ts.Close() 341 | client := newServerClient() 342 | _, err := client.Get(ts.URL) 343 | if err != nil { 344 | t.Errorf("cannot create request: %v", err) 345 | } 346 | want := fmt.Sprintf(golden(t.Name()), ts.URL, is.req.RemoteAddr, ts.Listener.Addr()) 347 | if got := buf.String(); got != want { 348 | t.Errorf(`expected input to contain "%v", got %v instead`, want, got) 349 | } 350 | } 351 | 352 | func TestIncomingSkipHeader(t *testing.T) { 353 | t.Parallel() 354 | logger := &Logger{ 355 | RequestHeader: true, 356 | RequestBody: true, 357 | ResponseHeader: true, 358 | ResponseBody: true, 359 | } 360 | var buf bytes.Buffer 361 | logger.SetOutput(&buf) 362 | logger.SkipHeader([]string{ 363 | "user-agent", 364 | "content-type", 365 | }) 366 | is := inspect(logger.Middleware(jsonHandler{}), 1) 367 | ts := httptest.NewServer(is) 368 | defer ts.Close() 369 | client := newServerClient() 370 | uri := fmt.Sprintf("%s/json", ts.URL) 371 | go func() { 372 | req, err := http.NewRequest(http.MethodGet, uri, nil) 373 | if err != nil { 374 | t.Errorf("cannot create request: %v", err) 375 | } 376 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 377 | if _, err = client.Do(req); err != nil { 378 | t.Errorf("cannot connect to the server: %v", err) 379 | } 380 | }() 381 | is.Wait() 382 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 383 | if got := buf.String(); got != want { 384 | t.Errorf("logged HTTP request %s; want %s", got, want) 385 | } 386 | } 387 | 388 | func TestIncomingBodyFilter(t *testing.T) { 389 | t.Parallel() 390 | logger := &Logger{ 391 | RequestHeader: true, 392 | RequestBody: true, 393 | ResponseHeader: true, 394 | ResponseBody: true, 395 | } 396 | var buf bytes.Buffer 397 | logger.SetOutput(&buf) 398 | logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { 399 | mediatype, _, _ := mime.ParseMediaType(h.Get("Content-Type")) 400 | return mediatype == "application/json", nil 401 | }) 402 | is := inspect(logger.Middleware(jsonHandler{}), 1) 403 | 404 | ts := httptest.NewServer(is) 405 | defer ts.Close() 406 | client := newServerClient() 407 | uri := fmt.Sprintf("%s/json", ts.URL) 408 | go func() { 409 | req, err := http.NewRequest(http.MethodGet, uri, nil) 410 | if err != nil { 411 | t.Errorf("cannot create request: %v", err) 412 | } 413 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 414 | if _, err = client.Do(req); err != nil { 415 | t.Errorf("cannot connect to the server: %v", err) 416 | } 417 | }() 418 | is.Wait() 419 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 420 | if got := buf.String(); got != want { 421 | t.Errorf("logged HTTP request %s; want %s", got, want) 422 | } 423 | } 424 | 425 | func TestIncomingBodyFilterSoftError(t *testing.T) { 426 | t.Parallel() 427 | logger := &Logger{ 428 | RequestHeader: true, 429 | RequestBody: true, 430 | ResponseHeader: true, 431 | ResponseBody: true, 432 | } 433 | var buf bytes.Buffer 434 | logger.SetOutput(&buf) 435 | logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { 436 | // filter anyway, but print soft error saying something went wrong during the filtering. 437 | return true, errors.New("incomplete implementation") 438 | }) 439 | is := inspect(logger.Middleware(jsonHandler{}), 1) 440 | 441 | ts := httptest.NewServer(is) 442 | defer ts.Close() 443 | client := newServerClient() 444 | uri := fmt.Sprintf("%s/json", ts.URL) 445 | go func() { 446 | req, err := http.NewRequest(http.MethodGet, uri, nil) 447 | if err != nil { 448 | t.Errorf("cannot create request: %v", err) 449 | } 450 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 451 | if _, err = client.Do(req); err != nil { 452 | t.Errorf("cannot connect to the server: %v", err) 453 | } 454 | }() 455 | is.Wait() 456 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 457 | if got := buf.String(); got != want { 458 | t.Errorf("logged HTTP request %s; want %s", got, want) 459 | } 460 | } 461 | 462 | func TestIncomingBodyFilterPanicked(t *testing.T) { 463 | t.Parallel() 464 | logger := &Logger{ 465 | RequestHeader: true, 466 | RequestBody: true, 467 | ResponseHeader: true, 468 | ResponseBody: true, 469 | } 470 | var buf bytes.Buffer 471 | logger.SetOutput(&buf) 472 | logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { 473 | panic("evil panic") 474 | }) 475 | is := inspect(logger.Middleware(jsonHandler{}), 1) 476 | ts := httptest.NewServer(is) 477 | defer ts.Close() 478 | 479 | client := newServerClient() 480 | uri := fmt.Sprintf("%s/json", ts.URL) 481 | go func() { 482 | req, err := http.NewRequest(http.MethodGet, uri, nil) 483 | if err != nil { 484 | t.Errorf("cannot create request: %v", err) 485 | } 486 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 487 | if _, err = client.Do(req); err != nil { 488 | t.Errorf("cannot connect to the server: %v", err) 489 | } 490 | }() 491 | is.Wait() 492 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 493 | if got := buf.String(); got != want { 494 | t.Errorf("logged HTTP request %s; want %s", got, want) 495 | } 496 | } 497 | 498 | func TestIncomingWithTimeRequest(t *testing.T) { 499 | t.Parallel() 500 | logger := &Logger{ 501 | Time: true, 502 | RequestHeader: true, 503 | RequestBody: true, 504 | ResponseHeader: true, 505 | ResponseBody: true, 506 | } 507 | var buf bytes.Buffer 508 | logger.SetOutput(&buf) 509 | 510 | is := inspect(logger.Middleware(helloHandler{}), 1) 511 | ts := httptest.NewServer(is) 512 | defer ts.Close() 513 | go func() { 514 | client := &http.Client{ 515 | Transport: newTransport(), 516 | } 517 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 518 | if err != nil { 519 | t.Errorf("cannot create request: %v", err) 520 | } 521 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 522 | if _, err = client.Do(req); err != nil { 523 | t.Errorf("cannot connect to the server: %v", err) 524 | } 525 | }() 526 | is.Wait() 527 | got := buf.String() 528 | if !strings.Contains(got, "* Request at ") { 529 | t.Error("missing printing start time of request") 530 | } 531 | if !strings.Contains(got, "* Request took ") { 532 | t.Error("missing printing request duration") 533 | } 534 | } 535 | 536 | func TestIncomingFormattedJSON(t *testing.T) { 537 | t.Parallel() 538 | logger := &Logger{ 539 | RequestHeader: true, 540 | RequestBody: true, 541 | ResponseHeader: true, 542 | ResponseBody: true, 543 | Formatters: []Formatter{ 544 | &JSONFormatter{}, 545 | }, 546 | } 547 | var buf bytes.Buffer 548 | logger.SetOutput(&buf) 549 | is := inspect(logger.Middleware(jsonHandler{}), 1) 550 | 551 | ts := httptest.NewServer(is) 552 | defer ts.Close() 553 | client := newServerClient() 554 | uri := fmt.Sprintf("%s/json", ts.URL) 555 | go func() { 556 | req, err := http.NewRequest(http.MethodGet, uri, nil) 557 | if err != nil { 558 | t.Errorf("cannot create request: %v", err) 559 | } 560 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 561 | if _, err = client.Do(req); err != nil { 562 | t.Errorf("cannot connect to the server: %v", err) 563 | } 564 | }() 565 | is.Wait() 566 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 567 | if got := buf.String(); got != want { 568 | t.Errorf("logged HTTP request %s; want %s", got, want) 569 | } 570 | } 571 | 572 | func TestIncomingBadJSON(t *testing.T) { 573 | t.Parallel() 574 | logger := &Logger{ 575 | RequestHeader: true, 576 | RequestBody: true, 577 | ResponseHeader: true, 578 | ResponseBody: true, 579 | Formatters: []Formatter{ 580 | &JSONFormatter{}, 581 | }, 582 | } 583 | var buf bytes.Buffer 584 | logger.SetOutput(&buf) 585 | is := inspect(logger.Middleware(badJSONHandler{}), 1) 586 | 587 | ts := httptest.NewServer(is) 588 | defer ts.Close() 589 | uri := fmt.Sprintf("%s/json", ts.URL) 590 | go func() { 591 | client := newServerClient() 592 | req, err := http.NewRequest(http.MethodGet, uri, nil) 593 | if err != nil { 594 | t.Errorf("cannot create request: %v", err) 595 | } 596 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 597 | if _, err = client.Do(req); err != nil { 598 | t.Errorf("cannot connect to the server: %v", err) 599 | } 600 | }() 601 | is.Wait() 602 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 603 | if got := buf.String(); got != want { 604 | t.Errorf("logged HTTP request %s; want %s", got, want) 605 | } 606 | } 607 | 608 | func TestIncomingFormatterPanicked(t *testing.T) { 609 | t.Parallel() 610 | logger := &Logger{ 611 | RequestHeader: true, 612 | RequestBody: true, 613 | ResponseHeader: true, 614 | ResponseBody: true, 615 | Formatters: []Formatter{ 616 | &panickingFormatter{}, 617 | }, 618 | } 619 | var buf bytes.Buffer 620 | logger.SetOutput(&buf) 621 | is := inspect(logger.Middleware(badJSONHandler{}), 1) 622 | 623 | ts := httptest.NewServer(is) 624 | defer ts.Close() 625 | uri := fmt.Sprintf("%s/json", ts.URL) 626 | go func() { 627 | client := newServerClient() 628 | req, err := http.NewRequest(http.MethodGet, uri, nil) 629 | if err != nil { 630 | t.Errorf("cannot create request: %v", err) 631 | } 632 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 633 | if _, err = client.Do(req); err != nil { 634 | t.Errorf("cannot connect to the server: %v", err) 635 | } 636 | }() 637 | is.Wait() 638 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 639 | if got := buf.String(); got != want { 640 | t.Errorf("logged HTTP request %s; want %s", got, want) 641 | } 642 | } 643 | 644 | func TestIncomingFormatterMatcherPanicked(t *testing.T) { 645 | t.Parallel() 646 | logger := &Logger{ 647 | RequestHeader: true, 648 | RequestBody: true, 649 | ResponseHeader: true, 650 | ResponseBody: true, 651 | Formatters: []Formatter{ 652 | &panickingFormatterMatcher{}, 653 | }, 654 | } 655 | var buf bytes.Buffer 656 | logger.SetOutput(&buf) 657 | is := inspect(logger.Middleware(badJSONHandler{}), 1) 658 | 659 | ts := httptest.NewServer(is) 660 | defer ts.Close() 661 | uri := fmt.Sprintf("%s/json", ts.URL) 662 | go func() { 663 | client := newServerClient() 664 | req, err := http.NewRequest(http.MethodGet, uri, nil) 665 | if err != nil { 666 | t.Errorf("cannot create request: %v", err) 667 | } 668 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 669 | if _, err = client.Do(req); err != nil { 670 | t.Errorf("cannot connect to the server: %v", err) 671 | } 672 | }() 673 | is.Wait() 674 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 675 | 676 | if got := buf.String(); got != want { 677 | t.Errorf("logged HTTP request %s; want %s", got, want) 678 | } 679 | } 680 | 681 | func TestIncomingForm(t *testing.T) { 682 | t.Parallel() 683 | logger := &Logger{ 684 | RequestHeader: true, 685 | RequestBody: true, 686 | ResponseHeader: true, 687 | ResponseBody: true, 688 | Formatters: []Formatter{ 689 | &JSONFormatter{}, 690 | }, 691 | } 692 | var buf bytes.Buffer 693 | logger.SetOutput(&buf) 694 | is := inspect(logger.Middleware(formHandler{}), 1) 695 | 696 | ts := httptest.NewServer(is) 697 | defer ts.Close() 698 | uri := fmt.Sprintf("%s/form", ts.URL) 699 | go func() { 700 | client := newServerClient() 701 | form := url.Values{} 702 | form.Add("foo", "bar") 703 | form.Add("email", "root@example.com") 704 | req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(form.Encode())) 705 | if err != nil { 706 | t.Errorf("cannot create request: %v", err) 707 | } 708 | if _, err = client.Do(req); err != nil { 709 | t.Errorf("cannot connect to the server: %v", err) 710 | } 711 | }() 712 | is.Wait() 713 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 714 | if got := buf.String(); got != want { 715 | t.Errorf("logged HTTP request %s; want %s", got, want) 716 | } 717 | } 718 | 719 | func TestIncomingBinaryBody(t *testing.T) { 720 | t.Parallel() 721 | logger := &Logger{ 722 | RequestHeader: true, 723 | RequestBody: true, 724 | ResponseHeader: true, 725 | ResponseBody: true, 726 | } 727 | var buf bytes.Buffer 728 | logger.SetOutput(&buf) 729 | is := inspect(logger.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 730 | w.Header()["Date"] = nil 731 | fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") 732 | })), 1) 733 | 734 | ts := httptest.NewServer(is) 735 | defer ts.Close() 736 | uri := fmt.Sprintf("%s/convert", ts.URL) 737 | go func() { 738 | client := newServerClient() 739 | b := []byte("RIFF\x00\x00\x00\x00WEBPVP") 740 | req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) 741 | if err != nil { 742 | t.Errorf("cannot create request: %v", err) 743 | } 744 | req.Header.Add("Content-Type", "image/webp") 745 | if _, err = client.Do(req); err != nil { 746 | t.Errorf("cannot connect to the server: %v", err) 747 | } 748 | }() 749 | is.Wait() 750 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 751 | if got := buf.String(); got != want { 752 | t.Errorf("logged HTTP request %s; want %s", got, want) 753 | } 754 | } 755 | 756 | func TestIncomingBinaryBodyNoMediatypeHeader(t *testing.T) { 757 | t.Parallel() 758 | logger := &Logger{ 759 | RequestHeader: true, 760 | RequestBody: true, 761 | ResponseHeader: true, 762 | ResponseBody: true, 763 | } 764 | var buf bytes.Buffer 765 | logger.SetOutput(&buf) 766 | is := inspect(logger.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 767 | w.Header()["Date"] = nil 768 | w.Header()["Content-Type"] = nil 769 | fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") 770 | })), 1) 771 | 772 | ts := httptest.NewServer(is) 773 | defer ts.Close() 774 | uri := fmt.Sprintf("%s/convert", ts.URL) 775 | go func() { 776 | client := newServerClient() 777 | b := []byte("RIFF\x00\x00\x00\x00WEBPVP") 778 | req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) 779 | if err != nil { 780 | t.Errorf("cannot create request: %v", err) 781 | } 782 | if _, err = client.Do(req); err != nil { 783 | t.Errorf("cannot connect to the server: %v", err) 784 | } 785 | }() 786 | is.Wait() 787 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 788 | if got := buf.String(); got != want { 789 | t.Errorf("logged HTTP request %s; want %s", got, want) 790 | } 791 | } 792 | 793 | func TestIncomingLongRequest(t *testing.T) { 794 | t.Parallel() 795 | logger := &Logger{ 796 | RequestHeader: true, 797 | RequestBody: true, 798 | ResponseHeader: true, 799 | ResponseBody: true, 800 | } 801 | var buf bytes.Buffer 802 | logger.SetOutput(&buf) 803 | is := inspect(logger.Middleware(longRequestHandler{}), 1) 804 | 805 | ts := httptest.NewServer(is) 806 | defer ts.Close() 807 | uri := fmt.Sprintf("%s/long-request", ts.URL) 808 | go func() { 809 | client := newServerClient() 810 | req, err := http.NewRequest(http.MethodPut, uri, strings.NewReader(petition)) 811 | if err != nil { 812 | t.Errorf("cannot create request: %v", err) 813 | } 814 | if _, err = client.Do(req); err != nil { 815 | t.Errorf("cannot connect to the server: %v", err) 816 | } 817 | }() 818 | is.Wait() 819 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr(), petition) 820 | if got := buf.String(); got != want { 821 | t.Errorf("logged HTTP request %s; want %s", got, want) 822 | } 823 | } 824 | 825 | func TestIncomingLongResponse(t *testing.T) { 826 | t.Parallel() 827 | logger := &Logger{ 828 | RequestHeader: true, 829 | RequestBody: true, 830 | ResponseHeader: true, 831 | ResponseBody: true, 832 | MaxResponseBody: int64(len(petition) + 1000), // value larger than the text 833 | } 834 | var buf bytes.Buffer 835 | logger.SetOutput(&buf) 836 | is := inspect(logger.Middleware(longResponseHandler{}), 1) 837 | 838 | ts := httptest.NewServer(is) 839 | defer ts.Close() 840 | uri := fmt.Sprintf("%s/long-response", ts.URL) 841 | go func() { 842 | client := newServerClient() 843 | req, err := http.NewRequest(http.MethodGet, uri, nil) 844 | if err != nil { 845 | t.Errorf("cannot create request: %v", err) 846 | } 847 | resp, err := client.Do(req) 848 | if err != nil { 849 | t.Errorf("cannot connect to the server: %v", err) 850 | } 851 | testBody(t, resp.Body, []byte(petition)) 852 | }() 853 | is.Wait() 854 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr(), petition) 855 | if got := buf.String(); got != want { 856 | t.Errorf("logged HTTP request %s; want %s", got, want) 857 | } 858 | } 859 | 860 | func TestIncomingLongResponseHead(t *testing.T) { 861 | t.Parallel() 862 | logger := &Logger{ 863 | RequestHeader: true, 864 | RequestBody: true, 865 | ResponseHeader: true, 866 | ResponseBody: true, 867 | MaxResponseBody: int64(len(petition) + 1000), // value larger than the text 868 | } 869 | var buf bytes.Buffer 870 | logger.SetOutput(&buf) 871 | is := inspect(logger.Middleware(longResponseHandler{}), 1) 872 | 873 | ts := httptest.NewServer(is) 874 | defer ts.Close() 875 | client := newServerClient() 876 | uri := fmt.Sprintf("%s/long-response", ts.URL) 877 | go func() { 878 | req, err := http.NewRequest(http.MethodHead, uri, nil) 879 | if err != nil { 880 | t.Errorf("cannot create request: %v", err) 881 | } 882 | if _, err = client.Do(req); err != nil { 883 | t.Errorf("cannot connect to the server: %v", err) 884 | } 885 | }() 886 | is.Wait() 887 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 888 | if got := buf.String(); got != want { 889 | t.Errorf("logged HTTP request %s; want %s", got, want) 890 | } 891 | } 892 | 893 | func TestIncomingTooLongResponse(t *testing.T) { 894 | t.Parallel() 895 | logger := &Logger{ 896 | RequestHeader: true, 897 | RequestBody: true, 898 | ResponseHeader: true, 899 | ResponseBody: true, 900 | MaxResponseBody: 5000, // value smaller than the text 901 | } 902 | var buf bytes.Buffer 903 | logger.SetOutput(&buf) 904 | is := inspect(logger.Middleware(longResponseHandler{}), 1) 905 | 906 | ts := httptest.NewServer(is) 907 | defer ts.Close() 908 | uri := fmt.Sprintf("%s/long-response", ts.URL) 909 | go func() { 910 | client := newServerClient() 911 | req, err := http.NewRequest(http.MethodGet, uri, nil) 912 | if err != nil { 913 | t.Errorf("cannot create request: %v", err) 914 | } 915 | resp, err := client.Do(req) 916 | if err != nil { 917 | t.Errorf("cannot connect to the server: %v", err) 918 | } 919 | testBody(t, resp.Body, []byte(petition)) 920 | }() 921 | is.Wait() 922 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 923 | if got := buf.String(); got != want { 924 | t.Errorf("logged HTTP request %s; want %s", got, want) 925 | } 926 | } 927 | 928 | func TestIncomingLongResponseUnknownLength(t *testing.T) { 929 | t.Parallel() 930 | logger := &Logger{ 931 | RequestHeader: true, 932 | RequestBody: true, 933 | ResponseHeader: true, 934 | ResponseBody: true, 935 | MaxResponseBody: 10000000, 936 | } 937 | var buf bytes.Buffer 938 | logger.SetOutput(&buf) 939 | 940 | repeat := 100 941 | is := inspect(logger.Middleware(longResponseUnknownLengthHandler{repeat: repeat}), 1) 942 | ts := httptest.NewServer(is) 943 | defer ts.Close() 944 | uri := fmt.Sprintf("%s/long-response", ts.URL) 945 | repeatedBody := strings.Repeat(petition, repeat+1) 946 | go func() { 947 | client := newServerClient() 948 | req, err := http.NewRequest(http.MethodGet, uri, nil) 949 | if err != nil { 950 | t.Errorf("cannot create request: %v", err) 951 | } 952 | resp, err := client.Do(req) 953 | if err != nil { 954 | t.Errorf("cannot connect to the server: %v", err) 955 | } 956 | testBody(t, resp.Body, []byte(repeatedBody)) 957 | }() 958 | is.Wait() 959 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr(), repeatedBody) 960 | if got := buf.String(); got != want { 961 | t.Errorf("logged HTTP request %s; want %s", got, want) 962 | } 963 | } 964 | 965 | func TestIncomingLongResponseUnknownLengthTooLong(t *testing.T) { 966 | t.Parallel() 967 | logger := &Logger{ 968 | RequestHeader: true, 969 | RequestBody: true, 970 | ResponseHeader: true, 971 | ResponseBody: true, 972 | MaxResponseBody: 5000, // value smaller than the text 973 | } 974 | var buf bytes.Buffer 975 | logger.SetOutput(&buf) 976 | is := inspect(logger.Middleware(longResponseUnknownLengthHandler{}), 1) 977 | 978 | ts := httptest.NewServer(is) 979 | defer ts.Close() 980 | uri := fmt.Sprintf("%s/long-response", ts.URL) 981 | go func() { 982 | client := newServerClient() 983 | req, err := http.NewRequest(http.MethodGet, uri, nil) 984 | if err != nil { 985 | t.Errorf("cannot create request: %v", err) 986 | } 987 | resp, err := client.Do(req) 988 | if err != nil { 989 | t.Errorf("cannot connect to the server: %v", err) 990 | } 991 | testBody(t, resp.Body, []byte(petition)) 992 | }() 993 | is.Wait() 994 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr()) 995 | if got := buf.String(); got != want { 996 | t.Errorf("logged HTTP request %s; want %s", got, want) 997 | } 998 | } 999 | 1000 | func TestIncomingMultipartForm(t *testing.T) { 1001 | t.Parallel() 1002 | logger := &Logger{ 1003 | RequestHeader: true, 1004 | // TODO(henvic): print request body once support for printing out multipart/formdata body is added. 1005 | ResponseHeader: true, 1006 | ResponseBody: true, 1007 | Formatters: []Formatter{ 1008 | &JSONFormatter{}, 1009 | }, 1010 | } 1011 | var buf bytes.Buffer 1012 | logger.SetOutput(&buf) 1013 | is := inspect(logger.Middleware(multipartHandler{t}), 1) 1014 | 1015 | ts := httptest.NewServer(is) 1016 | defer ts.Close() 1017 | uri := fmt.Sprintf("%s/multipart-upload", ts.URL) 1018 | body := &bytes.Buffer{} 1019 | writer := multipart.NewWriter(body) 1020 | multipartTestdata(writer, body) 1021 | go func() { 1022 | client := newServerClient() 1023 | req, err := http.NewRequest(http.MethodPost, uri, body) 1024 | if err != nil { 1025 | t.Errorf("cannot create request: %v", err) 1026 | } 1027 | req.Header.Set("Content-Type", writer.FormDataContentType()) 1028 | if _, err = client.Do(req); err != nil { 1029 | t.Errorf("cannot connect to the server: %v", err) 1030 | } 1031 | }() 1032 | is.Wait() 1033 | want := fmt.Sprintf(golden(t.Name()), uri, is.req.RemoteAddr, ts.Listener.Addr(), writer.FormDataContentType()) 1034 | if got := buf.String(); got != want { 1035 | t.Errorf("logged HTTP request %s; want %s", got, want) 1036 | } 1037 | } 1038 | 1039 | func TestIncomingTLS(t *testing.T) { 1040 | t.Parallel() 1041 | logger := &Logger{ 1042 | TLS: true, 1043 | RequestHeader: true, 1044 | RequestBody: true, 1045 | ResponseHeader: true, 1046 | ResponseBody: true, 1047 | } 1048 | var buf bytes.Buffer 1049 | logger.SetOutput(&buf) 1050 | is := inspect(logger.Middleware(helloHandler{}), 1) 1051 | 1052 | ts := httptest.NewTLSServer(is) 1053 | defer ts.Close() 1054 | go func() { 1055 | client := ts.Client() 1056 | req, err := http.NewRequest(http.MethodGet, ts.URL, nil) 1057 | if err != nil { 1058 | t.Errorf("cannot create request: %v", err) 1059 | } 1060 | req.Host = "example.com" // overriding the Host header to send 1061 | req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") 1062 | resp, err := client.Do(req) 1063 | if err != nil { 1064 | t.Errorf("cannot connect to the server: %v", err) 1065 | } 1066 | testBody(t, resp.Body, []byte("Hello, world!")) 1067 | }() 1068 | is.Wait() 1069 | want := fmt.Sprintf(golden(t.Name()), is.req.RemoteAddr) 1070 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1071 | t.Errorf("logged HTTP request %s; want %s", got, want) 1072 | } 1073 | } 1074 | 1075 | func TestIncomingMutualTLS(t *testing.T) { 1076 | t.Parallel() 1077 | caCert, err := os.ReadFile("testdata/cert.pem") 1078 | if err != nil { 1079 | panic(err) 1080 | } 1081 | clientCert, err := os.ReadFile("testdata/cert-client.pem") 1082 | if err != nil { 1083 | panic(err) 1084 | } 1085 | caCertPool := x509.NewCertPool() 1086 | caCertPool.AppendCertsFromPEM(caCert) 1087 | caCertPool.AppendCertsFromPEM(clientCert) 1088 | tlsConfig := &tls.Config{ 1089 | ClientCAs: caCertPool, 1090 | ClientAuth: tls.RequireAndVerifyClientCert, 1091 | } 1092 | logger := &Logger{ 1093 | TLS: true, 1094 | RequestHeader: true, 1095 | RequestBody: true, 1096 | ResponseHeader: true, 1097 | ResponseBody: true, 1098 | } 1099 | var buf bytes.Buffer 1100 | logger.SetOutput(&buf) 1101 | is := inspect(logger.Middleware(helloHandler{}), 1) 1102 | 1103 | // NOTE(henvic): Using httptest directly turned out complicated. 1104 | // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go 1105 | server := &http.Server{ 1106 | TLSConfig: tlsConfig, 1107 | Handler: is, 1108 | } 1109 | listener, err := netListener() 1110 | if err != nil { 1111 | panic(fmt.Sprintf("failed to listen on a port: %v", err)) 1112 | } 1113 | defer listener.Close() 1114 | go func() { 1115 | // Certificate generated with 1116 | // $ openssl req -x509 -newkey rsa:2048 \ 1117 | // -new -nodes -sha256 \ 1118 | // -days 36500 \ 1119 | // -out cert.pem \ 1120 | // -keyout key.pem \ 1121 | // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ 1122 | // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") 1123 | if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { 1124 | t.Errorf("server exit with unexpected error: %v", errcp) 1125 | } 1126 | }() 1127 | defer server.Shutdown(context.Background()) 1128 | 1129 | // Certificate generated with 1130 | // $ openssl req -newkey rsa:2048 \ 1131 | // -new -nodes -x509 \ 1132 | // -days 36500 \ 1133 | // -out cert-client.pem \ 1134 | // -keyout key-client.pem \ 1135 | // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" 1136 | cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") 1137 | if err != nil { 1138 | t.Errorf("failed to load X509 key pair: %v", err) 1139 | } 1140 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 1141 | if err != nil { 1142 | t.Errorf("failed to parse certificate for copying Leaf field") 1143 | } 1144 | 1145 | // Create a HTTPS client and supply the created CA pool and certificate 1146 | clientTLSConfig := &tls.Config{ 1147 | RootCAs: caCertPool, 1148 | Certificates: []tls.Certificate{cert}, 1149 | } 1150 | _, port, err := net.SplitHostPort(listener.Addr().String()) 1151 | if err != nil { 1152 | panic(err) 1153 | } 1154 | 1155 | host := fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) 1156 | go func() { 1157 | transport := newTransport() 1158 | transport.TLSClientConfig = clientTLSConfig 1159 | client := &http.Client{ 1160 | Transport: transport, 1161 | } 1162 | resp, err := client.Get(host) 1163 | if err != nil { 1164 | t.Errorf("cannot create request: %v", err) 1165 | } 1166 | testBody(t, resp.Body, []byte("Hello, world!")) 1167 | }() 1168 | is.Wait() 1169 | want := fmt.Sprintf(golden(t.Name()), host, is.req.RemoteAddr, port) 1170 | if got := buf.String(); !regexp.MustCompile(want).MatchString(got) { 1171 | t.Errorf("logged HTTP request %s; want %s", got, want) 1172 | } 1173 | } 1174 | 1175 | func TestIncomingMutualTLSNoSafetyLogging(t *testing.T) { 1176 | t.Parallel() 1177 | caCert, err := os.ReadFile("testdata/cert.pem") 1178 | if err != nil { 1179 | panic(err) 1180 | } 1181 | clientCert, err := os.ReadFile("testdata/cert-client.pem") 1182 | if err != nil { 1183 | panic(err) 1184 | } 1185 | caCertPool := x509.NewCertPool() 1186 | caCertPool.AppendCertsFromPEM(caCert) 1187 | caCertPool.AppendCertsFromPEM(clientCert) 1188 | tlsConfig := &tls.Config{ 1189 | ClientCAs: caCertPool, 1190 | ClientAuth: tls.RequireAndVerifyClientCert, 1191 | } 1192 | logger := &Logger{ 1193 | // TLS must be false 1194 | RequestHeader: true, 1195 | RequestBody: true, 1196 | ResponseHeader: true, 1197 | ResponseBody: true, 1198 | } 1199 | var buf bytes.Buffer 1200 | logger.SetOutput(&buf) 1201 | is := inspect(logger.Middleware(helloHandler{}), 1) 1202 | 1203 | // NOTE(henvic): Using httptest directly turned out complicated. 1204 | // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go 1205 | server := &http.Server{ 1206 | TLSConfig: tlsConfig, 1207 | Handler: is, 1208 | } 1209 | listener, err := netListener() 1210 | if err != nil { 1211 | panic(fmt.Sprintf("failed to listen on a port: %v", err)) 1212 | } 1213 | defer listener.Close() 1214 | go func() { 1215 | // Certificate generated with 1216 | // $ openssl req -x509 -newkey rsa:2048 \ 1217 | // -new -nodes -sha256 \ 1218 | // -days 36500 \ 1219 | // -out cert.pem \ 1220 | // -keyout key.pem \ 1221 | // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ 1222 | // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") 1223 | if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { 1224 | t.Errorf("server exit with unexpected error: %v", errcp) 1225 | } 1226 | }() 1227 | defer server.Shutdown(context.Background()) 1228 | 1229 | // Certificate generated with 1230 | // $ openssl req -newkey rsa:2048 \ 1231 | // -new -nodes -x509 \ 1232 | // -days 36500 \ 1233 | // -out cert-client.pem \ 1234 | // -keyout key-client.pem \ 1235 | // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" 1236 | cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") 1237 | if err != nil { 1238 | t.Errorf("failed to load X509 key pair: %v", err) 1239 | } 1240 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 1241 | if err != nil { 1242 | t.Errorf("failed to parse certificate for copying Leaf field") 1243 | } 1244 | // Create a HTTPS client and supply the created CA pool and certificate 1245 | clientTLSConfig := &tls.Config{ 1246 | RootCAs: caCertPool, 1247 | Certificates: []tls.Certificate{cert}, 1248 | } 1249 | _, port, err := net.SplitHostPort(listener.Addr().String()) 1250 | if err != nil { 1251 | panic(err) 1252 | } 1253 | host := fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) 1254 | go func() { 1255 | transport := newTransport() 1256 | transport.TLSClientConfig = clientTLSConfig 1257 | client := &http.Client{ 1258 | Transport: transport, 1259 | } 1260 | resp, err := client.Get(host) 1261 | if err != nil { 1262 | t.Errorf("cannot create request: %v", err) 1263 | } 1264 | testBody(t, resp.Body, []byte("Hello, world!")) 1265 | }() 1266 | is.Wait() 1267 | want := fmt.Sprintf(golden(t.Name()), host, is.req.RemoteAddr, port) 1268 | if got := buf.String(); got != want { 1269 | t.Errorf("logged HTTP request %s; want %s", got, want) 1270 | } 1271 | } 1272 | 1273 | func newServerClient() *http.Client { 1274 | return &http.Client{ 1275 | Transport: newTransport(), 1276 | } 1277 | } 1278 | -------------------------------------------------------------------------------- /testdata/cert-client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTDCCAjQCCQC9tIz6aPdvETANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJO 3 | TDEVMBMGA1UECAwMWnVpZC1Ib2xsYW5kMRIwEAYDVQQHDAlSb3R0ZXJkYW0xDzAN 4 | BgNVBAoMBkNsaWVudDENMAsGA1UECwwEVXNlcjENMAsGA1UEAwwEVXNlcjAgFw0y 5 | MDAxMjUyMDEyMzZaGA8yMTIwMDEwMTIwMTIzNlowZzELMAkGA1UEBhMCTkwxFTAT 6 | BgNVBAgMDFp1aWQtSG9sbGFuZDESMBAGA1UEBwwJUm90dGVyZGFtMQ8wDQYDVQQK 7 | DAZDbGllbnQxDTALBgNVBAsMBFVzZXIxDTALBgNVBAMMBFVzZXIwggEiMA0GCSqG 8 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54gG9rMrVioL9i3sHSkhE1iihNKAjRd+W 9 | S6iG60wNl7PeQcZvQhgf1d9/tZDyIM5mP0XaCRXxfRAHJeXDqjeLkGyqPPRNFgvU 10 | kzJDnKisZ7cPANqfWHJZz/qQF+ePvAnZiBEhp+9BXUfjiAHIMglvwn3W3s54d+0V 11 | DzmgZp1ha1LG/iU2MkHDpcNM/C8edtd9UmGG0mIHS0H1JpNfuRNjPeFdCVvsi0wO 12 | ZpIDWqzKiyCbro0IakxXStMIFwwpFEGgxkOytpJeqjdnEklTXxb0gxCthB9WKszz 13 | RsQfZ5UVW9guhYfp/a49cbcMO//qM3E6V/Pff9bLjkNLyC0ggi2BAgMBAAEwDQYJ 14 | KoZIhvcNAQELBQADggEBAC4RtmjCTFKKHJu51ic7vtIiH4Xc2nidAajrxPdb5a7C 15 | 9jYPmCcH4atbv3ce4VFJ0Fcq3M3MS2mIei9Y+vn1GfkLOe8zdT1hWmDLgZttj06x 16 | L5Zxm0hTcMz9miJp6HQ+JRZNSqgk0OnGLaT3W5fYEI68Aei1udAI2pGPRD1OwRwJ 17 | r/qSHUF8K0He2pbaL/czkfI5hADicawGNggalSF8rxmmzSc+qXvL1vk0BjqqUdES 18 | SvGZNabw7PfFAHWwl1BU3/xJRTL9X/xoZnQjlf+ljdab7MP3Je5He2wj9PCgJzA9 19 | HYl9VyKwmqLR/7Z941A+k9Xtf4/ykxL7Tr7qZAtRrIE= 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /testdata/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrTCCApWgAwIBAgIJAJH7PYwIA1bFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV 3 | BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRowGAYDVQQHDBFDYXJtZWwtYnkt 4 | dGhlLVNlYTEOMAwGA1UECgwFUGxpZmsxDjAMBgNVBAsMBUNsb3VkMRIwEAYDVQQD 5 | DAlsb2NhbGhvc3QwIBcNMjAwODEyMjIyMDQ1WhgPMjEyMDA3MTkyMjIwNDVaMHIx 6 | CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRowGAYDVQQHDBFDYXJt 7 | ZWwtYnktdGhlLVNlYTEOMAwGA1UECgwFUGxpZmsxDjAMBgNVBAsMBUNsb3VkMRIw 8 | EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB 9 | AQCvoOFz4m47kl5mw8In6LFC1Tanmo1vG4SscoU9XCg4Pht33qXE3CGOTQuNKtNm 10 | VVO4P1eCCjd8fwaRr0V04Wg/RK+AhneGxhMuQlebJxEEE4e4AoFyJzYeqcm6HLdK 11 | D2SLP9icajOUnF5ZerYns32sU34/htSqA8jdDBNoND0kPwCckvYGQu875n2V1BdJ 12 | LEYAyv1oOPG9Ec45nkyBApv9102WxICMf35O5XOKcegkp1g75D/ModNGNJ49k7ZL 13 | AajbRq5jkToo+u8LDkGmdavPO625bE7Roo24fAjUTVD/mYKAKzbqDtsrDOfyq8LM 14 | cH0Br/29vjCb/jKoe4e5A+j3AgMBAAGjRDBCMBQGA1UdEQQNMAuCCWxvY2FsaG9z 15 | dDALBgNVHQ8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0G 16 | CSqGSIb3DQEBCwUAA4IBAQAKdhOh1nxe0xZD3czfDyrCB1bals6rnREg4xvKnpc6 17 | 99F61S8+4SSbJIIWkP5BjNiP36czxi0h0yyXjO+WP9gnGHdcZmEwtXVHTMoC7Ql/ 18 | gmJ+xvls7NF08lOCnNjzh5Vb+2bEtckFSV2v0m1BsngSfcVLmZHxw44Hxa0nBAbi 19 | 1tYunESZxaJrB4snJHrvYfctTHa08XWVoAXNkZj/4fpHAoulkXfK+zczU/UVnWnW 20 | WJGHd8Beo6OE6AQ4RNewKW8K+jBv4CAu+wA5713O3Ys/GMxDZdS02Em4/IjFObFE 21 | YrHRtvpSrTxACFsw8XQbFy525XBoEWepqiZHRry+6ddl 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /testdata/cert_example.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDADCCAeigAwIBAgIQOLOnQdiwgDrspBPLtc9dBzANBgkqhkiG9w0BAQsFADAS 3 | MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw 4 | MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 5 | MIIBCgKCAQEAqkMAE2yt/xG/N9W4hVayxCWjr0idQQy6Roirdp8lk7rwH6heQZFB 6 | EyZuysonJAA/aEAt3l9914UMwlA80o9UeTc+CNqnP6oK02XBq8kbLwYQYSp1rUnD 7 | G2GioerFVE9si/lIRkt5c8XY5ocEKCdnh+ETzu0o18ClAFYwEywJaAuUQ9xFpe+j 8 | oaqOiy1DjAc/14MNuoBhUpiDLhwTrarMX0npzELp5nd8jmwYsN3YRsHMJ7HbfcN/ 9 | WNP475E28o+5MM7xh9hDuOKeRBlRz0jToR/pFVK+1aFaRToz7An2H/NUYgVLHqMw 10 | Hfd+T31zXOy+nfEvXYWnqiaL6+79W2krEQIDAQABo1AwTjAOBgNVHQ8BAf8EBAMC 11 | AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAWBgNVHREE 12 | DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAWhbA93uT9XyqaeSQ 13 | M2OsZY+4Y8wuyn0A7u6/YPhhb3HcWYIZ7BXjp/1f8JhZV32eLQ+qdK3ERAnHT0uR 14 | xVPsZKL1hwZkp4uIBAZ2x883EcZ+nZkN46BpKAfcOWdQLFuX3LUXWCgbkPf1Y/sc 15 | Oec7RCBF2qDlVKTHXmaoTQq4WXtKBrE5ekwxmU+/qrdwXkzQ9HNQWAlRCdHEtoEh 16 | cXwjtLBIvlessb7pptqaB7qTosxzfIF63ES+hhkpVEUvzDYJyuMWZgGjFNV/X+cp 17 | WhdIpMySdNWSd9Qc+YO4bRC96XecrAvLT3pULuQVrWohTZtep3zsgS3jJW7ilD+w 18 | g1oa4A== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /testdata/certold.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDeDCCAmACCQCElHz7U8/oHjANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJV 3 | UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEa 4 | MBgGA1UECgwRWW91ciBPcmdhbml6YXRpb24xEjAQBgNVBAsMCVlvdXIgVW5pdDES 5 | MBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMDExOTEwMzE1NloXDTMwMDExNjEwMzE1 6 | NlowfjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM 7 | DU1vdW50YWluIFZpZXcxGjAYBgNVBAoMEVlvdXIgT3JnYW5pemF0aW9uMRIwEAYD 8 | VQQLDAlZb3VyIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN 9 | AQEBBQADggEPADCCAQoCggEBAL8t4Wx+PMt7EacmWC6aIufg9mtT32H/su1nON6V 10 | j4yaoKtRNx2gvxkVL9SoW2ooS8E+5vl/VI/o9FAzwetLCL6qa+TAdu+DSmdxTGFF 11 | fVVHpw4yvLvbPfFoCCgd/LkFZZBT+gvb7EikUT5RjIRsCmIL6+ZvOGzkOzEOlHi7 12 | itJML3MvecpQWS0zyRbbwA1M0jUHeq/6Pt7PPbw94T6IHRFC+iJg4Wuz6IjvBUO9 13 | 41mUUF0f9qKHYmctjf2RngYRG025j3yzQDfQXfc0NVvWvk2M+WM4701Wvep8wjtO 14 | wQQ42bWD1FoytVGYbuYmu+TaovNYiYwr9vXXS/KIR5lwWB8CAwEAATANBgkqhkiG 15 | 9w0BAQsFAAOCAQEAqRizyUM1ja5MZHfh6dbPtFIrQcinhHVPvOs8qpa91D6LAhXb 16 | i5Yd/oVJwDsez562myAu04uMg3vl5vv2fuL3HA34aqPigdA7ITFiJQdwTawKjlWc 17 | Wgu6MAP17G8AcKpdqtVETU4dpkBRtVhg0CXShpWsylmE0h5ig7uibIRRY0YoT44x 18 | Bp5A8zxmEV3mO9qfx4gqsGJOlt8oFaYvr0BAemSb5lEg92hBEbaEewTDbWDV0u/C 19 | KxP8ndYb/EYitMEh5MFWR1/PPZsHBJbWbSTjJYXWeq8fizx+TW7UVxNgb9FvsGv2 20 | 9nql/a9eUSRSv63AORTPbPAMvUr6UNHGfERAPw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /testdata/key-client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC54gG9rMrVioL9 3 | i3sHSkhE1iihNKAjRd+WS6iG60wNl7PeQcZvQhgf1d9/tZDyIM5mP0XaCRXxfRAH 4 | JeXDqjeLkGyqPPRNFgvUkzJDnKisZ7cPANqfWHJZz/qQF+ePvAnZiBEhp+9BXUfj 5 | iAHIMglvwn3W3s54d+0VDzmgZp1ha1LG/iU2MkHDpcNM/C8edtd9UmGG0mIHS0H1 6 | JpNfuRNjPeFdCVvsi0wOZpIDWqzKiyCbro0IakxXStMIFwwpFEGgxkOytpJeqjdn 7 | EklTXxb0gxCthB9WKszzRsQfZ5UVW9guhYfp/a49cbcMO//qM3E6V/Pff9bLjkNL 8 | yC0ggi2BAgMBAAECggEACjjXh6q87MlVMsQ828XF+6MbUOIn/EiXZxh6CBFgeU7i 9 | YVKUqwGjefy08bz2X5pWP2EzYi4lusX536rB2+S8cTxb/XCkrqiLtgDyPq2ayQBb 10 | HMQbQbAHedDqIopt/YWFtSS6bHNjwOB0V5rfHjdCNZcofGx8RjuyGfpgXOXHudeo 11 | 5FTs+EBvuMRTOqxL6Pv8t5Q5761zJVZOyiUv5HzunXKPARHp+Y/si+6vwCrnShDq 12 | 0tDVX/zbZ3hkGrKMq0MvVkhbH5gil9BjcPmWkTEjfWuXuR8wB15oXltvWJPriv7f 13 | ILlKQxBmVbIfqjJvh7ShKUO3Cc8rV4KLe1bzUtgKkQKBgQD28bWRKX8OjXK1zwy1 14 | +FOfiv7gBnt4avw1QCFMCBewu5RE5pAK6eMKE6Ef+a7vY44MkaGMzAu0mrKGMpP1 15 | jjT8Yg2BoB9lMDrFIMA5rfhwJY7n1/qA5xIeCJDB5EZExQN4ql6BVETH/MIe/TtB 16 | m5DrTqhULMZfB+QcJIZSSY9GywKBgQDAsw8XhICghAwgmYeVL84kqBoBFPuBtKP+ 17 | wKvq4aiNY6wi4c8w8OgJcrpjD3Lz4tqisNvVRifMj7eVor+nGQkjC5mwLREyTKAV 18 | 1gQ+C7VOi/braVylEkwhFQ9jZvAmn7rQoNHb8L0P3um+ZKkb6uTym3/eYeAHDxvg 19 | RXx69fbHYwKBgB2gvG8RMnxVfjjQAa9nfuj6bUAFpxS4iU/+RMBxjB4ZM13c59VX 20 | YHUaC8/hThrMsANUCbTx2kmt8dNmCBiDGlpZjVNLGdkzIyn5lvaUp+UUrIOmhxim 21 | IKdX0b5hnAiuNo9oqXQM3z+7VLMRIOXrO0TwKAQJZzeJo9W4kCEZUEZnAoGBAJ08 22 | foAOGnbfyJWBMWTGUUsP78gaOu8nWvmwdZd+8m4MepUr9EhXCr9K4lOac44V+Zju 23 | /zITwL3mN0LePcw3XYE/IfTjkTid1bJ7o5KNMzAYfS6yFmqLd5s2+AuAH00k4OcD 24 | kroIwfyFQ+2bbXHeRVrBD6GB869O4MwrZttegDNJAoGAWEvoSRcD0+n16vAN9fk7 25 | tDdXAEcixH4jFbGlmsys+jrF2mndGRA1J40kjMW7H4xnF6b2ruXQkJtNcNN3Mvzy 26 | dwGkcsj+EcHg3AVxi7XrKgmyt+B34svNuB0uQxvshNflfvMmMdfm2GGngOsxp7rR 27 | hWJUNRVtNWmH+W0jGjuDFME= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /testdata/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCvoOFz4m47kl5m 3 | w8In6LFC1Tanmo1vG4SscoU9XCg4Pht33qXE3CGOTQuNKtNmVVO4P1eCCjd8fwaR 4 | r0V04Wg/RK+AhneGxhMuQlebJxEEE4e4AoFyJzYeqcm6HLdKD2SLP9icajOUnF5Z 5 | erYns32sU34/htSqA8jdDBNoND0kPwCckvYGQu875n2V1BdJLEYAyv1oOPG9Ec45 6 | nkyBApv9102WxICMf35O5XOKcegkp1g75D/ModNGNJ49k7ZLAajbRq5jkToo+u8L 7 | DkGmdavPO625bE7Roo24fAjUTVD/mYKAKzbqDtsrDOfyq8LMcH0Br/29vjCb/jKo 8 | e4e5A+j3AgMBAAECggEANrpFRt06SGn17MP3joQeKJtUKqoohITotOwCxPogtlX0 9 | LUg+E7gc5MDxZo3/zhWsvu9OD4GrhKn4nBEn7aIH4B9BKSW9vUuf0nxt3DUyQjjr 10 | w9VUDQRXAvsZl1s3amadiB7fGu6lIBwR8oQgmwJ9mONzpcwYHNqNDwSiT4hnvRE4 11 | RjQscjrqKYDkXekpk+3uPom3e5+UZXL0VoqjsB3PGN8xC3H8VFPcolPVfeKBhcxy 12 | zAOxvBfIyzKcooTtUn8UpCCqhCh+Ak/wHi08FptsSWj+FiWaE/d2OYXl3al+w4Tf 13 | CQApuaFcfsgzCtVeGDgPH0JIJKgLg/QLy9Z8x+zr8QKBgQDlw735Eb1Aszt7phJm 14 | y7/VE/FW3RwD0gyfr9kAlxxWjWwMfsEjmg4+A/bNbf5/G4OYmD7dZy3iF7pD/aQi 15 | kLwYgb04VVxz1xEjbBfaZhe1MBJKHQHkpBtKGBDPi1GYu2dFDkh6vqQX1/mvIfvn 16 | +B7I2BOvL9Fw37RvblbiJ2JHxQKBgQDDrq6YpBo4+H8cICyy6HuhyF9UMYVf+mwG 17 | lNCTW0bhMl1XRg4/KKS8JB3PR//KTq/c6pc/WYZu81FdEY9bRmN1lXcgTWXC5nWp 18 | P0ZNISJ71q2Lbz3cW0npvBh/Q4abmPdEHYgk+A7Z4vlTZw7DETb2FS9n6hvHrtsG 19 | 8gKkiSk9iwKBgAMuCVQIHdFmaZ1VeA26JiaBxyZHmxqmboxLN7qdXMQJ4wPtQSkH 20 | +ch77496RTpnHBQhj0UrJ2RopahJO1tLG39PVFoSPFxSDqep2E6qeQuF5crmyd7r 21 | MoF9AcaNjAyME2rOPsyMFONLluYIl17nfS2UZ/lVtRVV0z5zjXpFx0NtAoGAL2Wm 22 | QK6u81GtaCCa8xLAr2UbQgdkqOS9ObLd+nNHbdCHL1Z2qPGtRSzyU3y7BkOc8UOZ 23 | Muz6VPF2qbZRJOiduqNjYV2d4mFz6nS7EH+QHLLZAkcFktRByO2YeWrftdyNN+B3 24 | U40J+9iwT3VM7A7FY0GqY98er3U49Cu2XCgk5xUCgYByAmWl2Z9J7HrsqIT695c2 25 | R5AKmwhIIsF5UnbPPfmwAbgdxY0ZZE/X3Ec4WF9UlXOZMdOyZNAuFaPEf6jCaB5V 26 | 1uMGsLtVr4L+tHz+Fh+8EM36otCShyWq9PIE3dDXtkFVxAj4EMW/DamTQfqyj74d 27 | yClFOSq7k1xxrC1dvf1V5w== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /testdata/key_example.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqQwATbK3/Eb83 3 | 1biFVrLEJaOvSJ1BDLpGiKt2nyWTuvAfqF5BkUETJm7KyickAD9oQC3eX33XhQzC 4 | UDzSj1R5Nz4I2qc/qgrTZcGryRsvBhBhKnWtScMbYaKh6sVUT2yL+UhGS3lzxdjm 5 | hwQoJ2eH4RPO7SjXwKUAVjATLAloC5RD3EWl76Ohqo6LLUOMBz/Xgw26gGFSmIMu 6 | HBOtqsxfSenMQunmd3yObBiw3dhGwcwnsdt9w39Y0/jvkTbyj7kwzvGH2EO44p5E 7 | GVHPSNOhH+kVUr7VoVpFOjPsCfYf81RiBUseozAd935PfXNc7L6d8S9dhaeqJovr 8 | 7v1baSsRAgMBAAECggEADGW8h62OLdh49/PT78GUWrvy4zyCVs46chBZi9WiwtMF 9 | 0QhNdLDC8EYIIzP9DZ4G/+xMarjBTQQfHbcB9sMA/6KKHdLuArC7ARGTvJJ0LERg 10 | xPJ2hxur3T6KvQd/PthZqweHv7aXLVrmpEKIhvP3kelNq++Q3cTlPtUHwx2dwbmX 11 | seDtsUIOStdeijJBchqQcxwft5KeXQDYxJSK5KzAmLxSVnyDdU+sXbwOtXlw252d 12 | /NHAC+U01dNlxsYoeguTrkqOX52DI5awAoykB3cStWQiPb1bN+0Qa6ORvXUX/wKD 13 | TYT0yhdkxu/OmmWy+wVNIMS9uIRP8bPOwVyFihIdUQKBgQDJgTYfbE8G6+uOklQw 14 | 4ohI8yXDsQT7wGIZuBeFwQO4t/k5B5ymE9tZ169ka7KHM3I5OBpC3eHpQiaWvyOH 15 | 1psjdEIehhnIO/Hm1rELOlRFWKLHBuFLoqxvDQjUddWMhddZvJetBwhDiX4AzL7p 16 | cnAyeVclIn43Fk277ETHy63L6wKBgQDYTr1Y9uep91HqpOcKRiR8tA1P2NxJDTZR 17 | IucvOiMJ6tw4cys0D2JSfaKAQigv+P+pjRyXrYwkaRDB/gEk587wntE9wPIMs/2c 18 | 95Cxp3Uwsvwv52kHn4WH1DJfMSvASe84qNkANWw0jjhjbXvxhZ3M1i2KfCdjYTfZ 19 | YLI1NTIR8wKBgQCyz4JjqA0Iq1nAjoE/UAZ4FawxV2iArltfT0kwW/Mde8Qgo2yS 20 | w5QmyYrOpfMqnrCBrhM/uv25rAXqR3sUE5Bfic8SnxVJ5kfm/CTnPb+COgFYc/aA 21 | 074IXZy0TExQAoTzELPXyyG+LMgvlYDkT7TYVWzLeyxdXeFlHWh7k3aKOQKBgCQM 22 | a03qSA1xZDuAo+h4bBhEQXuvHncmNokrEfAy9ifu9iiKOQcCEVbCDVTmsZ/dFW6C 23 | T+OPTq26vMo3tKUb5McBEMoD39LyJDAGqhyRVdx518F8BWr50N0kJgjrPula6P0+ 24 | VnvMa24OzaL0WhWUOQosH4bWzhGn4BDgJpLrfJ61AoGACFkdXnHBgdU8lvbf6TIX 25 | w6EtfkjPikSMyFojTvoUtJQuMHldUocuYwpuHlfp0ubqmaqSrRbFQwxpE5Qe19gQ 26 | yvh6jsNAChuhQACbJv29PzO65J3zgqPWjQOz+2LVdx1YkXgK+IF1xuDC8DCBmJCn 27 | v7mn8Euw5b3Y4iZ47Kd9ZIw= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /testdata/keyold.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/LeFsfjzLexGn 3 | JlgumiLn4PZrU99h/7LtZzjelY+MmqCrUTcdoL8ZFS/UqFtqKEvBPub5f1SP6PRQ 4 | M8HrSwi+qmvkwHbvg0pncUxhRX1VR6cOMry72z3xaAgoHfy5BWWQU/oL2+xIpFE+ 5 | UYyEbApiC+vmbzhs5DsxDpR4u4rSTC9zL3nKUFktM8kW28ANTNI1B3qv+j7ezz28 6 | PeE+iB0RQvoiYOFrs+iI7wVDveNZlFBdH/aih2JnLY39kZ4GERtNuY98s0A30F33 7 | NDVb1r5NjPljOO9NVr3qfMI7TsEEONm1g9RaMrVRmG7mJrvk2qLzWImMK/b110vy 8 | iEeZcFgfAgMBAAECggEBAIs2sr9ZUZXu4VTxZCdTUHW/6FERe0oWc8nSb6QODsEZ 9 | XEREWLk3c9ClD/ZwPlkYTMnEr1chdIdy4G2CswRO2GhXG0gxKqqQ1V5sL21pt7Gh 10 | ArIhGjRFm16uHbpw07Y7itDFhgCavf3Lwel6YrOPJSRuf/KGLPWGYOABOPaNwrIN 11 | SouNd/8mxBc57vbrgx1bUFS7vt09vqGKsQYDxvOP1a5UPvwqqq9H4lEx9LrVwAte 12 | Pm6gDrjPbADfSCBWGZdOwnDiY59HY/R4la5d2d6sGoAa73vQZCDJEtpuDh0LOFOS 13 | IeTPP+erAK71y5p07aplMNPrCUtk/4f3Gd/WKAPiLoECgYEA89b9nR8tzkn64BXZ 14 | hK+gB1OFTNc9ACzIQkh7/Z88z6txqQ1QOPOA8TJXPs1bNZg1soTQ/nwMAEN/SbSq 15 | O2Z6WMAnh0j/ATIs0rYTO/sS7ZjV67tXRWzkEei9P6Wm3NJ05DfcyvbBt1ScrEjB 16 | JN7D4/r1sACP+fFam/Swu+Yj1kECgYEAyLaXpXzzHZRZ3Kq3WKJaB08ydQlERoaX 17 | RJMLAS17LWS8BNDjBl3IQwnnTR4zB0TELgQszWRsMrOOCGDhid8owt21XogA+yi9 18 | Z4XsR4nKh8m64Vp8d3DVAxJo+c+OYdfhA8rWptHvfa4SeZbjGPoybgyjFgUhYI63 19 | Ty1dMqGqVl8CgYEAhjb7J8Xmp5qO7VL5hJBKzF2LjM0YdYUwwVM2dFZ22XPrvvpm 20 | AsL9YUWtQhM0th5OyDFU/A55aJe+c2pvHPz+MOWrnEpwmk7s3xp7IdPECmXKsdNP 21 | aRZTvwvVRzg9zWRGFOwuqsUBwZBgIHB3Z3z6Y/1ZyIO2vAO+NQONWA+IAEECgYBl 22 | yQsgVjwoDPqBSGXQYgzL1iLdbUSdi1Wc5gDXqQvlWkdrHc9zhA2xyYzt89mm3v2p 23 | 5F4gDsQ79giaQR8/Ptc58xsuBESTGfbrT+Qh50O5FtlZvPyPyb2MYEKyJMqs3cBz 24 | nuK6GI6eKq+dz6H9Iax/WJM/8HwbrmRRl8zCh2+NewKBgEOQ5GU/kayXBWssdrN1 25 | VeyNYI9Gd8/Rr65vN8xxYoZabi/EC08daQBka+f73AShZboY0JOfAQNKp3R33Gf1 26 | 2PLP52mfQ4zeuhAcqOnX0NCrSBXFzx/+1kfAnKP50StfV0Ke4mdeqtFTZ9W+XsUC 27 | IMbePLH0bQhynrKsJWiGXtwv 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /testdata/log.txtar: -------------------------------------------------------------------------------- 1 | Golden files for the test cases. 2 | -- TestIncoming -- 3 | * Request to http://%s/ 4 | * Request from %s 5 | > GET / HTTP/1.1 6 | > Host: %s 7 | > Accept-Encoding: gzip 8 | > User-Agent: Robot/0.1 crawler@example.com 9 | 10 | < HTTP/1.1 200 OK 11 | 12 | Hello, world! 13 | -- TestIncomingBadJSON -- 14 | * Request to %s 15 | * Request from %s 16 | > GET /json HTTP/1.1 17 | > Host: %s 18 | > Accept-Encoding: gzip 19 | > User-Agent: Robot/0.1 crawler@example.com 20 | 21 | < HTTP/1.1 200 OK 22 | < Content-Type: application/json; charset=utf-8 23 | 24 | * body cannot be formatted: invalid character '}' looking for beginning of value 25 | {"bad": } 26 | -- TestIncomingBinaryBody -- 27 | * Request to %s 28 | * Request from %s 29 | > POST /convert HTTP/1.1 30 | > Host: %s 31 | > Accept-Encoding: gzip 32 | > Content-Length: 14 33 | > Content-Type: image/webp 34 | > User-Agent: Go-http-client/1.1 35 | 36 | * body contains binary data 37 | < HTTP/1.1 200 OK 38 | 39 | * body contains binary data 40 | -- TestIncomingBinaryBodyNoMediatypeHeader -- 41 | * Request to %s 42 | * Request from %s 43 | > POST /convert HTTP/1.1 44 | > Host: %s 45 | > Accept-Encoding: gzip 46 | > Content-Length: 14 47 | > User-Agent: Go-http-client/1.1 48 | 49 | * body contains binary data 50 | < HTTP/1.1 200 OK 51 | 52 | * body contains binary data 53 | -- TestIncomingBodyFilter -- 54 | * Request to %s 55 | * Request from %s 56 | > GET /json HTTP/1.1 57 | > Host: %s 58 | > Accept-Encoding: gzip 59 | > User-Agent: Robot/0.1 crawler@example.com 60 | 61 | < HTTP/1.1 200 OK 62 | < Content-Type: application/json; charset=utf-8 63 | 64 | -- TestIncomingBodyFilterPanicked -- 65 | * Request to %s 66 | * Request from %s 67 | > GET /json HTTP/1.1 68 | > Host: %s 69 | > Accept-Encoding: gzip 70 | > User-Agent: Robot/0.1 crawler@example.com 71 | 72 | * panic while filtering body: evil panic 73 | < HTTP/1.1 200 OK 74 | < Content-Type: application/json; charset=utf-8 75 | 76 | * panic while filtering body: evil panic 77 | {"result":"Hello, world!","number":3.14} 78 | -- TestIncomingBodyFilterSoftError -- 79 | * Request to %s 80 | * Request from %s 81 | > GET /json HTTP/1.1 82 | > Host: %s 83 | > Accept-Encoding: gzip 84 | > User-Agent: Robot/0.1 crawler@example.com 85 | 86 | * error on request body filter: incomplete implementation 87 | < HTTP/1.1 200 OK 88 | < Content-Type: application/json; charset=utf-8 89 | 90 | * error on response body filter: incomplete implementation 91 | -- TestIncomingConcurrency -- 92 | > GET / HTTP/1.1 93 | > Host: %s 94 | > Accept-Encoding: gzip 95 | > User-Agent: Robot/0.1 crawler@example.com 96 | 97 | < HTTP/1.1 200 OK 98 | 99 | Hello, world! 100 | -- TestIncomingFilterPanicked -- 101 | * cannot filter request: GET /: panic: evil panic 102 | * Request to %v/ 103 | * Request from %v 104 | > GET / HTTP/1.1 105 | > Host: %v 106 | > Accept-Encoding: gzip 107 | > User-Agent: Go-http-client/1.1 108 | 109 | < HTTP/1.1 200 OK 110 | 111 | Hello, world! 112 | -- TestIncomingForm -- 113 | * Request to %s 114 | * Request from %s 115 | > POST /form HTTP/1.1 116 | > Host: %s 117 | > Accept-Encoding: gzip 118 | > Content-Length: 32 119 | > User-Agent: Go-http-client/1.1 120 | 121 | email=root%%40example.com&foo=bar 122 | < HTTP/1.1 200 OK 123 | 124 | form received 125 | -- TestIncomingFormattedJSON -- 126 | * Request to %s 127 | * Request from %s 128 | > GET /json HTTP/1.1 129 | > Host: %s 130 | > Accept-Encoding: gzip 131 | > User-Agent: Robot/0.1 crawler@example.com 132 | 133 | < HTTP/1.1 200 OK 134 | < Content-Type: application/json; charset=utf-8 135 | 136 | { 137 | "result": "Hello, world!", 138 | "number": 3.14 139 | } 140 | -- TestIncomingFormatterMatcherPanicked -- 141 | * Request to %s 142 | * Request from %s 143 | > GET /json HTTP/1.1 144 | > Host: %s 145 | > Accept-Encoding: gzip 146 | > User-Agent: Robot/0.1 crawler@example.com 147 | 148 | < HTTP/1.1 200 OK 149 | < Content-Type: application/json; charset=utf-8 150 | 151 | * panic while testing body format: evil matcher 152 | {"bad": } 153 | -- TestIncomingFormatterPanicked -- 154 | * Request to %s 155 | * Request from %s 156 | > GET /json HTTP/1.1 157 | > Host: %s 158 | > Accept-Encoding: gzip 159 | > User-Agent: Robot/0.1 crawler@example.com 160 | 161 | < HTTP/1.1 200 OK 162 | < Content-Type: application/json; charset=utf-8 163 | 164 | * body cannot be formatted: panic: evil formatter 165 | {"bad": } 166 | -- TestIncomingLongRequest -- 167 | * Request to %s 168 | * Request from %s 169 | > PUT /long-request HTTP/1.1 170 | > Host: %s 171 | > Accept-Encoding: gzip 172 | > Content-Length: 9846 173 | > User-Agent: Go-http-client/1.1 174 | 175 | %s 176 | < HTTP/1.1 200 OK 177 | 178 | long request received 179 | -- TestIncomingLongResponse -- 180 | * Request to %s 181 | * Request from %s 182 | > GET /long-response HTTP/1.1 183 | > Host: %s 184 | > Accept-Encoding: gzip 185 | > User-Agent: Go-http-client/1.1 186 | 187 | < HTTP/1.1 200 OK 188 | < Content-Length: 9846 189 | 190 | %s 191 | -- TestIncomingLongResponseHead -- 192 | * Request to %s 193 | * Request from %s 194 | > HEAD /long-response HTTP/1.1 195 | > Host: %s 196 | > User-Agent: Go-http-client/1.1 197 | 198 | < HTTP/1.1 200 OK 199 | < Content-Length: 9846 200 | 201 | -- TestIncomingLongResponseUnknownLength -- 202 | * Request to %s 203 | * Request from %s 204 | > GET /long-response HTTP/1.1 205 | > Host: %s 206 | > Accept-Encoding: gzip 207 | > User-Agent: Go-http-client/1.1 208 | 209 | < HTTP/1.1 200 OK 210 | 211 | %s 212 | -- TestIncomingLongResponseUnknownLengthTooLong -- 213 | * Request to %s 214 | * Request from %s 215 | > GET /long-response HTTP/1.1 216 | > Host: %s 217 | > Accept-Encoding: gzip 218 | > User-Agent: Go-http-client/1.1 219 | 220 | < HTTP/1.1 200 OK 221 | 222 | * body is too long (9846 bytes) to print, skipping (longer than 5000 bytes) 223 | -- TestIncomingMinimal -- 224 | * Request to %s 225 | * Request from %s 226 | -- TestIncomingMultipartForm -- 227 | * Request to %s 228 | * Request from %s 229 | > POST /multipart-upload HTTP/1.1 230 | > Host: %s 231 | > Accept-Encoding: gzip 232 | > Content-Length: 10355 233 | > Content-Type: %s 234 | > User-Agent: Go-http-client/1.1 235 | 236 | < HTTP/1.1 200 OK 237 | 238 | upload received 239 | -- TestIncomingMutualTLS -- 240 | ^\* Request to %s 241 | \* Request from %s 242 | \* TLS connection using TLS \d.\d / \w+ 243 | \* ALPN: h2 accepted 244 | \* Client certificate: 245 | \* subject: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL 246 | \* start date: Sat Jan 25 20:12:36 UTC 2020 247 | \* expire date: Mon Jan 1 20:12:36 UTC 2120 248 | \* issuer: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL 249 | > GET /mutual-tls-test HTTP/2\.0 250 | > Host: localhost:%s 251 | > Accept-Encoding: gzip 252 | > User-Agent: Go-http-client/2\.0 253 | 254 | < HTTP/2\.0 200 OK 255 | 256 | Hello, world! 257 | -- TestIncomingMutualTLSNoSafetyLogging -- 258 | * Request to %s 259 | * Request from %s 260 | > GET /mutual-tls-test HTTP/2.0 261 | > Host: localhost:%s 262 | > Accept-Encoding: gzip 263 | > User-Agent: Go-http-client/2.0 264 | 265 | < HTTP/2.0 200 OK 266 | 267 | Hello, world! 268 | -- TestIncomingNotFound -- 269 | * Request to http://%s/ 270 | * Request from %s 271 | > GET / HTTP/1.1 272 | > Host: %s 273 | > Accept-Encoding: gzip 274 | > User-Agent: Robot/0.1 crawler@example.com 275 | 276 | < HTTP/1.1 404 Not Found 277 | < Content-Type: text/plain; charset=utf-8 278 | < X-Content-Type-Options: nosniff 279 | 280 | -- TestIncomingSanitized -- 281 | * Request to %s 282 | * Request from %s 283 | > GET /incoming HTTP/1.1 284 | > Host: %s 285 | > Accept-Encoding: gzip 286 | > Cookie: food=████████████████████ 287 | > User-Agent: Robot/0.1 crawler@example.com 288 | 289 | < HTTP/1.1 200 OK 290 | 291 | Hello, world! 292 | -- TestIncomingSkipHeader -- 293 | * Request to %s 294 | * Request from %s 295 | > GET /json HTTP/1.1 296 | > Host: %s 297 | > Accept-Encoding: gzip 298 | 299 | < HTTP/1.1 200 OK 300 | 301 | {"result":"Hello, world!","number":3.14} 302 | -- TestIncomingTLS -- 303 | ^\* Request to https://example\.com/ 304 | \* Request from %s 305 | \* TLS connection using TLS \d+\.\d+ / \w+ 306 | > GET / HTTP/1\.1 307 | > Host: example\.com 308 | > Accept-Encoding: gzip 309 | > User-Agent: Robot/0\.1 crawler@example\.com 310 | 311 | < HTTP/1\.1 200 OK 312 | 313 | Hello, world! 314 | -- TestIncomingTooLongResponse -- 315 | * Request to %s 316 | * Request from %s 317 | > GET /long-response HTTP/1.1 318 | > Host: %s 319 | > Accept-Encoding: gzip 320 | > User-Agent: Go-http-client/1.1 321 | 322 | < HTTP/1.1 200 OK 323 | < Content-Length: 9846 324 | 325 | * body is too long (9846 bytes) to print, skipping (longer than 5000 bytes) 326 | -- TestOutgoing -- 327 | * Request to %s 328 | > GET / HTTP/1.1 329 | > Host: %s 330 | > User-Agent: Robot/0.1 crawler@example.com 331 | 332 | < HTTP/1.1 200 OK 333 | < Content-Length: 13 334 | < Content-Type: text/plain; charset=utf-8 335 | 336 | Hello, world! 337 | -- TestOutgoingBadJSON -- 338 | * Request to %s 339 | > GET /json HTTP/1.1 340 | > Host: %s 341 | > User-Agent: Robot/0.1 crawler@example.com 342 | 343 | < HTTP/1.1 200 OK 344 | < Content-Length: 9 345 | < Content-Type: application/json; charset=utf-8 346 | 347 | * body cannot be formatted: invalid character '}' looking for beginning of value 348 | {"bad": } 349 | -- TestOutgoingBinaryBody -- 350 | * Request to %s 351 | > POST /convert HTTP/1.1 352 | > Host: %s 353 | > Content-Length: 14 354 | > Content-Type: image/webp 355 | 356 | * body contains binary data 357 | < HTTP/1.1 200 OK 358 | < Content-Length: 16 359 | < Content-Type: application/pdf 360 | 361 | * body contains binary data 362 | -- TestOutgoingBinaryBodyNoMediatypeHeader -- 363 | * Request to %s 364 | > POST /convert HTTP/1.1 365 | > Host: %s 366 | > Content-Length: 14 367 | 368 | * body contains binary data 369 | < HTTP/1.1 200 OK 370 | < Content-Length: 16 371 | 372 | * body contains binary data 373 | -- TestOutgoingBodyFilter -- 374 | * Request to %s 375 | > GET /json HTTP/1.1 376 | > Host: %s 377 | > User-Agent: Robot/0.1 crawler@example.com 378 | 379 | < HTTP/1.1 200 OK 380 | < Content-Length: 40 381 | < Content-Type: application/json; charset=utf-8 382 | 383 | -- TestOutgoingBodyFilterPanicked -- 384 | * Request to %s 385 | > GET /json HTTP/1.1 386 | > Host: %s 387 | > User-Agent: Robot/0.1 crawler@example.com 388 | 389 | < HTTP/1.1 200 OK 390 | < Content-Length: 40 391 | < Content-Type: application/json; charset=utf-8 392 | 393 | * panic while filtering body: evil panic 394 | {"result":"Hello, world!","number":3.14} 395 | -- TestOutgoingBodyFilterSoftError -- 396 | * Request to %s 397 | > GET /json HTTP/1.1 398 | > Host: %s 399 | > User-Agent: Robot/0.1 crawler@example.com 400 | 401 | < HTTP/1.1 200 OK 402 | < Content-Length: 40 403 | < Content-Type: application/json; charset=utf-8 404 | 405 | * error on response body filter: incomplete implementation 406 | -- TestOutgoingConcurrency -- 407 | * Request to %s 408 | > GET / HTTP/1.1 409 | > Host: %s 410 | > User-Agent: Robot/0.1 crawler@example.com 411 | 412 | < HTTP/1.1 200 OK 413 | < Content-Length: 13 414 | < Content-Type: text/plain; charset=utf-8 415 | 416 | Hello, world! 417 | -- TestOutgoingFilterPanicked -- 418 | * cannot filter request: GET %v: panic: evil panic 419 | * Request to %v 420 | > GET / HTTP/1.1 421 | > Host: %v 422 | 423 | < HTTP/1.1 200 OK 424 | < Content-Length: 13 425 | < Content-Type: text/plain; charset=utf-8 426 | 427 | Hello, world! 428 | -- TestOutgoingForm -- 429 | * Request to %s 430 | > POST /form HTTP/1.1 431 | > Host: %s 432 | > Content-Length: 32 433 | 434 | email=root%%40example.com&foo=bar 435 | < HTTP/1.1 200 OK 436 | < Content-Length: 13 437 | < Content-Type: text/plain; charset=utf-8 438 | 439 | form received 440 | -- TestOutgoingFormattedJSON/json -- 441 | * Request to %s 442 | > GET /json HTTP/1.1 443 | > Host: %s 444 | > User-Agent: Robot/0.1 crawler@example.com 445 | 446 | < HTTP/1.1 200 OK 447 | < Content-Length: 40 448 | < Content-Type: application/json; charset=utf-8 449 | 450 | { 451 | "result": "Hello, world!", 452 | "number": 3.14 453 | } 454 | -- TestOutgoingFormattedJSON/vnd -- 455 | * Request to %s 456 | > GET /vnd HTTP/1.1 457 | > Host: %s 458 | > User-Agent: Robot/0.1 crawler@example.com 459 | 460 | < HTTP/1.1 200 OK 461 | < Content-Length: 40 462 | < Content-Type: application/vnd.api+json 463 | 464 | { 465 | "result": "Hello, world!", 466 | "number": 3.14 467 | } 468 | -- TestOutgoingFormatterMatcherPanicked -- 469 | * Request to %s 470 | > GET /json HTTP/1.1 471 | > Host: %s 472 | > User-Agent: Robot/0.1 crawler@example.com 473 | 474 | < HTTP/1.1 200 OK 475 | < Content-Length: 9 476 | < Content-Type: application/json; charset=utf-8 477 | 478 | * panic while testing body format: evil matcher 479 | {"bad": } 480 | -- TestOutgoingFormatterPanicked -- 481 | * Request to %s 482 | > GET /json HTTP/1.1 483 | > Host: %s 484 | > User-Agent: Robot/0.1 crawler@example.com 485 | 486 | < HTTP/1.1 200 OK 487 | < Content-Length: 9 488 | < Content-Type: application/json; charset=utf-8 489 | 490 | * body cannot be formatted: panic: evil formatter 491 | {"bad": } 492 | -- TestOutgoingHTTP2MutualTLS -- 493 | ^\* Request to %s 494 | \* Client certificate: 495 | \* subject: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL 496 | \* start date: Sat Jan 25 20:12:36 UTC 2020 497 | \* expire date: Mon Jan 1 20:12:36 UTC 2120 498 | \* issuer: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL 499 | > GET /mutual-tls-test HTTP/1\.1 500 | > Host: localhost:%s 501 | 502 | \* TLS connection using TLS \d+\.\d+ / \w+ 503 | \* ALPN: h2 accepted 504 | \* Server certificate: 505 | \* subject: CN=localhost,OU=Cloud,O=Plifk,L=Carmel-by-the-Sea,ST=California,C=US 506 | \* start date: Wed Aug 12 22:20:45 UTC 2020 507 | \* expire date: Fri Jul 19 22:20:45 UTC 2120 508 | \* issuer: CN=localhost,OU=Cloud,O=Plifk,L=Carmel-by-the-Sea,ST=California,C=US 509 | \* TLS certificate verify ok\. 510 | < HTTP/2\.0 200 OK 511 | < Content-Length: 13 512 | < Content-Type: text/plain; charset=utf-8 513 | 514 | Hello, world! 515 | -- TestOutgoingHTTP2MutualTLSNoSafetyLogging -- 516 | * Request to %s 517 | > GET /mutual-tls-test HTTP/1.1 518 | > Host: localhost:%s 519 | 520 | < HTTP/2.0 200 OK 521 | < Content-Length: 13 522 | < Content-Type: text/plain; charset=utf-8 523 | 524 | Hello, world! 525 | -- TestOutgoingLongRequest -- 526 | * Request to %s 527 | > PUT /long-request HTTP/1.1 528 | > Host: %s 529 | > Content-Length: 9846 530 | 531 | %s 532 | < HTTP/1.1 200 OK 533 | < Content-Length: 21 534 | < Content-Type: text/plain; charset=utf-8 535 | 536 | long request received 537 | -- TestOutgoingLongResponse -- 538 | * Request to %s 539 | > GET /long-response HTTP/1.1 540 | > Host: %s 541 | 542 | < HTTP/1.1 200 OK 543 | < Content-Length: 9846 544 | < Content-Type: text/plain; charset=utf-8 545 | 546 | %s 547 | -- TestOutgoingLongResponseHead -- 548 | * Request to %s 549 | > HEAD /long-response HTTP/1.1 550 | > Host: %s 551 | 552 | < HTTP/1.1 200 OK 553 | < Content-Length: 9846 554 | 555 | -- TestOutgoingLongResponseUnknownLength -- 556 | * Request to %s 557 | > GET /long-response HTTP/1.1 558 | > Host: %s 559 | 560 | < HTTP/1.1 200 OK 561 | < Content-Type: text/plain; charset=utf-8 562 | 563 | %s 564 | -- TestOutgoingLongResponseUnknownLengthTooLong -- 565 | * Request to %s 566 | > GET /long-response HTTP/1.1 567 | > Host: %s 568 | 569 | < HTTP/1.1 200 OK 570 | < Content-Type: text/plain; charset=utf-8 571 | 572 | * body is too long, skipping (contains more than 4096 bytes) 573 | -- TestOutgoingMultipartForm -- 574 | * Request to %s 575 | > POST /multipart-upload HTTP/1.1 576 | > Host: %s 577 | > Content-Length: 10355 578 | > Content-Type: %s 579 | 580 | < HTTP/1.1 200 OK 581 | < Content-Length: 15 582 | < Content-Type: text/plain; charset=utf-8 583 | 584 | upload received 585 | -- TestOutgoingSanitized -- 586 | * Request to %s 587 | > GET / HTTP/1.1 588 | > Host: %s 589 | > Cookie: food=████████████████████ 590 | > User-Agent: Robot/0.1 crawler@example.com 591 | 592 | < HTTP/1.1 200 OK 593 | < Content-Length: 13 594 | < Content-Type: text/plain; charset=utf-8 595 | 596 | Hello, world! 597 | -- TestOutgoingSkipHeader -- 598 | * Request to %s 599 | > GET /json HTTP/1.1 600 | > Host: %s 601 | 602 | < HTTP/1.1 200 OK 603 | < Content-Length: 40 604 | 605 | {"result":"Hello, world!","number":3.14} 606 | -- TestOutgoingSkipSanitize -- 607 | * Request to %s 608 | > GET / HTTP/1.1 609 | > Host: %s 610 | > Cookie: food=sorbet 611 | > User-Agent: Robot/0.1 crawler@example.com 612 | 613 | < HTTP/1.1 200 OK 614 | < Content-Length: 13 615 | < Content-Type: text/plain; charset=utf-8 616 | 617 | Hello, world! 618 | -- TestOutgoingTLS -- 619 | ^\* Request to %s 620 | > GET / HTTP/1\.1 621 | > Host: example\.com 622 | > User-Agent: Robot/0\.1 crawler@example\.com 623 | 624 | \* TLS connection using TLS \d+\.\d+ / \w+ 625 | \* Server certificate: 626 | \* subject: O=Acme Co 627 | \* start date: Thu Jan 1 00:00:00 UTC 1970 628 | \* expire date: Sat Jan 29 16:00:00 UTC 2084 629 | \* issuer: O=Acme Co 630 | \* TLS certificate verify ok\. 631 | < HTTP/1\.1 200 OK 632 | < Content-Length: 13 633 | < Content-Type: text/plain; charset=utf-8 634 | 635 | Hello, world! 636 | -- TestOutgoingTLSBadClientCertificate -- 637 | * Request to %s 638 | * Client certificate: 639 | * subject: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL 640 | * start date: Sat Jan 25 20:12:36 UTC 2020 641 | * expire date: Mon Jan 1 20:12:36 UTC 2120 642 | * issuer: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL 643 | > GET / HTTP/1.1 644 | > Host: example.com 645 | > User-Agent: Robot/0.1 crawler@example.com 646 | 647 | * remote error: tls: %s 648 | -- TestOutgoingTLSInsecureSkipVerify -- 649 | ^\* Request to %s 650 | \* Skipping TLS verification: connection is susceptible to man-in-the-middle attacks\. 651 | > GET / HTTP/1\.1 652 | > Host: example\.com 653 | > User-Agent: Robot/0\.1 crawler@example\.com 654 | 655 | \* TLS connection using TLS \d+\.\d+ / \w+ \(insecure=true\) 656 | \* Server certificate: 657 | \* subject: O=Acme Co 658 | \* start date: Thu Jan 1 00:00:00 UTC 1970 659 | \* expire date: Sat Jan 29 16:00:00 UTC 2084 660 | \* issuer: O=Acme Co 661 | \* TLS certificate verify ok\. 662 | < HTTP/1\.1 200 OK 663 | < Content-Length: 13 664 | < Content-Type: text/plain; charset=utf-8 665 | 666 | Hello, world! 667 | -- TestOutgoingTLSInvalidCertificate -- 668 | ^\* Request to %s 669 | > GET / HTTP/1\.1 670 | > Host: example\.com 671 | > User-Agent: Robot/0\.1 crawler@example\.com 672 | 673 | \* .*x509: .+ 674 | -- TestOutgoingTooLongResponse -- 675 | * Request to %s 676 | > GET /long-response HTTP/1.1 677 | > Host: %s 678 | 679 | < HTTP/1.1 200 OK 680 | < Content-Length: 9846 681 | < Content-Type: text/plain; charset=utf-8 682 | 683 | * body is too long (9846 bytes) to print, skipping (longer than 5000 bytes) 684 | -- TestOutgoingProxy -- 685 | \* Request to %s 686 | \* Using proxy: %s 687 | > GET / HTTP/1.1 688 | > Host: example.com 689 | > User-Agent: Robot/0.1 crawler@example.com 690 | 691 | < HTTP/1.1 200 OK 692 | < Content-Length: 13 693 | < Content-Type: text/plain; charset=utf-8 694 | 695 | Hello, world! -------------------------------------------------------------------------------- /testdata/petition.golden: -------------------------------------------------------------------------------- 1 | Pétition 2 | 3 | des fabricants de chandelles, bougies, lampes, chandeliers, réverbères, mouchettes, éteignoirs, et des producteurs de suif, huile, résine, alcool, et généralement de tout ce qui concerne l’éclairage. 4 | 5 | 6 | 7 | À MM. les Membres de la Chambre des Députés. 8 | 9 | 10 | « Messieurs, 11 | « Vous êtes dans la bonne voie. Vous repoussez les théories abstraites ; l’abondance, le bon marché vous touchent peu. Vous vous préoccupez surtout du sort du producteur. Vous le voulez affranchir de la concurrence extérieure, en un mot, vous voulez réserver le marché national au travail national. 12 | 13 | « Nous venons vous offrir une admirable occasion d’appliquer votre… comment dirons-nous ? votre théorie ? non, rien n’est plus trompeur que la théorie ; votre doctrine ? votre système ? votre principe ? mais vous n’aimez pas les doctrines, vous avez horreur des systèmes, et, quant aux principes, vous déclarez qu’il n’y en a pas en économie sociale ; nous dirons donc votre pratique, votre pratique sans théorie et sans principe. 14 | 15 | « Nous subissons l’intolérable concurrence d’un rival étranger placé, à ce qu’il paraît, dans des conditions tellement supérieures aux nôtres, pour la production de la lumière, qu’il en inonde notre marché national à un prix fabuleusement réduit ; car, aussitôt qu’il se montre, notre vente cesse, tous les consommateurs s’adressent à lui, et une branche d’industrie française, dont les ramifications sont innombrables, est tout à coup frappée de la stagnation la plus complète. Ce rival, qui n’est autre que le soleil, nous fait une guerre si acharnée, que nous soupçonnons qu’il nous est suscité par la perfide Albion (bonne diplomatie par le temps qui court !), d’autant qu’il a pour cette île orgueilleuse des ménagements dont il se dispense envers nous. 16 | 17 | « Nous demandons qu’il vous plaise de faire une loi qui ordonne la fermeture de toutes fenêtres, lucarnes, abat-jour, contre-vents, volets, rideaux, vasistas, œils-de-bœuf, stores, en un mot, de toutes ouvertures, trous, fentes et fissures par lesquelles la lumière du soleil a coutume de pénétrer dans les maisons, au préjudice des belles industries dont nous nous flattons d’avoir doté le pays, qui ne saurait sans ingratitude nous abandonner aujourd’hui à une lutte si inégale. 18 | 19 | « Veuillez, Messieurs les députés, ne pas prendre notre demande pour une satire, et ne la repoussez pas du moins sans écouter les raisons que nous avons à faire valoir à l’appui. 20 | 21 | « Et d’abord, si vous fermez, autant que possible, tout accès à la lumière naturelle, si vous créez ainsi le besoin de lumière artificielle, quelle est en France l’industrie qui, de proche en proche, ne sera pas encouragée ? 22 | 23 | « S’il se consomme plus de suif, il faudra plus de bœufs et de moutons, et, par suite, on verra se multiplier les prairies artificielles, la viande, la laine, le cuir, et surtout les engrais, cette base de toute richesse agricole. 24 | 25 | « S’il se consomme plus d’huile, on verra s’étendre la culture du pavot, de l’olivier, du colza. Ces plantes riches et épuisantes viendront à propos mettre à profit cette fertilité que l’élève des bestiaux aura communiquée à notre territoire. 26 | 27 | « Nos landes se couvriront d’arbres résineux. De nombreux essaims d’abeilles recueilleront sur nos montagnes des trésors parfumés qui s’évaporent aujourd’hui sans utilité, comme les fleurs d’où ils émanent. Il n’est donc pas une branche d’agriculture qui ne prenne un grand développement. 28 | 29 | « Il en est de même de la navigation : des milliers de vaisseaux iront à la pêche de la baleine, et dans peu de temps nous aurons une marine capable de soutenir l’honneur de la France et de répondre à la patriotique susceptibilité des pétitionnaires soussignés, marchands de chandelles, etc. 30 | 31 | « Mais que dirons-nous de l’article Paris ? Voyez d’ici les dorures, les bronzes, les cristaux en chandeliers, en lampes, en lustres, en candélabres, briller dans de spacieux magasins auprès desquels ceux d’aujourd’hui ne sont que des boutiques. 32 | 33 | « Il n’est pas jusqu’au pauvre résinier, au sommet de sa dune, ou au triste mineur, au fond de sa noire galerie, qui ne voie augmenter son salaire et son bien-être. 34 | 35 | « Veuillez y réfléchir, Messieurs ; et vous resterez convaincus qu’il n’est peut-être pas un Français, depuis l’opulent actionnaire d’Anzin jusqu’au plus humble débitant d’allumettes, dont le succès de notre demande n’améliore la condition. 36 | 37 | « Nous prévoyons vos objections, Messieurs ; mais vous ne nous en opposerez pas une seule que vous n’alliez la ramasser dans les livres usés des partisans de la liberté commerciale. Nous osons vous mettre au défi de prononcer un mot contre nous qui ne se retourne à l’instant contre vous-mêmes et contre le principe qui dirige toute votre politique. 38 | 39 | « Nous direz-vous que, si nous gagnons à cette protection, la France n’y gagnera point, parce que le consommateur en fera les frais ? 40 | 41 | « Nous vous répondrons : 42 | 43 | « Vous n’avez plus le droit d’invoquer les intérêts du consommateur. Quand il s’est trouvé aux prises avec le producteur, en toutes circonstances vous l’avez sacrifié. — Vous l’avez fait pour encourager le travail, pour accroître le domaine du travail. Par le même motif, vous devez le faire encore. 44 | 45 | « Vous avez été vous-mêmes au-devant de l’objection. Lorsqu’on vous disait : le consommateur est intéressé à la libre introduction du fer, de la houille, du sésame, du froment, des tissus. — Oui, disiez-vous, mais le producteur est intéressé à leur exclusion. — Eh bien ! si les consommateurs sont intéressés à l’admission de la lumière naturelle, les producteurs le sont à son interdiction. 46 | 47 | « Mais, disiez-vous encore, le producteur et le consommateur ne font qu’un. Si le fabricant gagne par la protection, il fera gagner l’agriculteur. Si l’agriculture prospère, elle ouvrira des débouchés aux fabriques. — Eh bien ! si vous nous conférez le monopole de l’éclairage pendant le jour, d’abord nous achèterons beaucoup de suifs, de charbons, d’huiles, de résines, de cire, d’alcool, d’argent, de fer, de bronzes, de cristaux, pour alimenter notre industrie, et, de plus, nous et nos nombreux fournisseurs, devenus riches, nous consommerons beaucoup et répandrons l’aisance dans toutes les branches du travail national. 48 | 49 | « Direz-vous que la lumière du soleil est un don gratuit, et que repousser des dons gratuits, ce serait repousser la richesse même sous prétexte d’encourager les moyens de l’acquérir ? 50 | 51 | « Mais prenez garde que vous portez la mort dans le cœur de votre politique ; prenez garde que jusqu’ici vous avez toujours repoussé le produit étranger parce qu’il se rapproche du don gratuit, et d’autant plus qu’il se rapproche du don gratuit. Pour obtempérer aux exigences des autres monopoleurs, vous n’aviez qu’un demi-motif ; pour accueillir notre demande, vous avez un motif complet, et nous repousser précisément en vous fondant sur ce que nous sommes plus fondés que les autres, ce serait poser l’équation : + × + = – ; en d’autres termes, ce serait entasser absurdité sur absurdité. 52 | 53 | « Le travail et la nature concourent en proportions diverses, selon les pays et les climats, à la création d’un produit. La part qu’y met la nature est toujours gratuite ; c’est la part du travail qui en fait la valeur et se paie. 54 | 55 | « Si une orange de Lisbonne se vend à moitié prix d’une orange de Paris, c’est qu’une chaleur naturelle et par conséquent gratuite fait pour l’une ce que l’autre doit à une chaleur artificielle et partant coûteuse. 56 | 57 | « Donc, quand une orange nous arrive de Portugal, on peut dire qu’elle nous est donnée moitié gratuitement, moitié à titre onéreux, ou, en d’autres termes, à moitié prix relativement à celle de Paris. 58 | 59 | « Or, c’est précisément de cette demi-gratuité (pardon du mot) que vous arguez pour l’exclure. Vous dites : Comment le travail national pourrait-il soutenir la concurrence du travail étranger quand celui-là a tout à faire, et que celui-ci n’a à accomplir que la moitié de la besogne, le soleil se chargeant du reste ? — Mais si la demi-gratuité vous détermine à repousser la concurrence, comment la gratuité entière vous porterait-elle à admettre la concurrence ? Ou vous n’êtes pas logiciens, ou vous devez, repoussant la demi-gratuité comme nuisible à notre travail national, repousser a fortiori et avec deux fois plus de zèle la gratuité entière. 60 | 61 | « Encore une fois, quand un produit, houille, fer, froment ou tissu, nous vient du dehors et que nous pouvons l’acquérir avec moins de travail que si nous le faisions nous-mêmes, la différence est un don gratuit qui nous est conféré. Ce don est plus ou moins considérable, selon que la différence est plus ou moins grande. Il est du quart, de moitié, des trois quarts de la valeur du produit, si l’étranger ne nous demande que les trois quarts, la moitié, le quart du paiement. Il est aussi complet qu’il puisse l’être, quand le donateur, comme fait le soleil pour la lumière, ne nous demande rien. La question, et nous la posons formellement, est de savoir si vous voulez pour la France le bénéfice de la consommation gratuite ou les prétendus avantages de la production onéreuse. Choisissez, mais soyez logiques ; car, tant que vous repousserez, comme vous le faites, la houille, le fer, le froment, les tissus étrangers, en proportion de ce que leur prix se rapproche de zéro, quelle inconséquence ne serait-ce pas d’admettre la lumière du soleil, dont le prix est à zéro, pendant toute la journée ? » -------------------------------------------------------------------------------- /tls.go: -------------------------------------------------------------------------------- 1 | package httpretty 2 | 3 | // A list of cipher suite IDs that are, or have been, implemented by the 4 | // crypto/tls package. 5 | // See https://www.iana.org/assignments/tls-parameters/tls-parameters.xml 6 | // See https://github.com/golang/go/blob/c2edcf4b1253fdebc13df8a25979904c3ef01c66/src/crypto/tls/cipher_suites.go 7 | var tlsCiphers = map[uint16]string{ 8 | // TLS 1.0 - 1.2 cipher suites. 9 | 0x0005: "TLS_RSA_WITH_RC4_128_SHA", 10 | 0x000a: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", 11 | 0x002f: "TLS_RSA_WITH_AES_128_CBC_SHA", 12 | 0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA", 13 | 0x003c: "TLS_RSA_WITH_AES_128_CBC_SHA256", 14 | 0x009c: "TLS_RSA_WITH_AES_128_GCM_SHA256", 15 | 0x009d: "TLS_RSA_WITH_AES_256_GCM_SHA384", 16 | 0xc007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", 17 | 0xc009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 18 | 0xc00a: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 19 | 0xc011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA", 20 | 0xc012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", 21 | 0xc013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 22 | 0xc014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 23 | 0xc023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 24 | 0xc027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 25 | 0xc02f: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 26 | 0xc02b: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 27 | 0xc030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 28 | 0xc02c: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 29 | 0xcca8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 30 | 0xcca9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", 31 | 32 | // TLS 1.3 cipher suites. 33 | 0x1301: "TLS_AES_128_GCM_SHA256", 34 | 0x1302: "TLS_AES_256_GCM_SHA384", 35 | 0x1303: "TLS_CHACHA20_POLY1305_SHA256", 36 | 37 | // TLS_FALLBACK_SCSV isn't a standard cipher suite but an indicator 38 | // that the client is doing version fallback. See RFC 7507. 39 | 0x5600: "TLS_FALLBACK_SCSV", 40 | } 41 | 42 | // List of TLS protocol versions supported by Go. 43 | // See https://github.com/golang/go/blob/f4a8bf128364e852cff87cf404a5c16c457ef8f6/src/crypto/tls/common.go 44 | var tlsProtocolVersions = map[uint16]string{ 45 | 0x0301: "TLS 1.0", 46 | 0x0302: "TLS 1.1", 47 | 0x0303: "TLS 1.2", 48 | 0x0304: "TLS 1.3", 49 | } 50 | --------------------------------------------------------------------------------