pax_global_header 0000666 0000000 0000000 00000000064 13725530515 0014520 g ustar 00root root 0000000 0000000 52 comment=8fe0a500eec05acdeede69884d2fddacad1ea557 httpretty-0.0.6/ 0000775 0000000 0000000 00000000000 13725530515 0013572 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/.github/ 0000775 0000000 0000000 00000000000 13725530515 0015132 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/.github/FUNDING.yml 0000664 0000000 0000000 00000000017 13725530515 0016745 0 ustar 00root root 0000000 0000000 github: henvic httpretty-0.0.6/.gitignore 0000664 0000000 0000000 00000000527 13725530515 0015566 0 ustar 00root root 0000000 0000000 # Docs doc/*.md doc/*.1 # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof .cover coverage.html coverage.out *.coverprofile httpretty-0.0.6/.travis.yml 0000664 0000000 0000000 00000001311 13725530515 0015677 0 ustar 00root root 0000000 0000000 language: go go: - "1.15.x" # when updating to a new major release, update scripts below too - "1.14.x" - "1.13.x" - "tip" os: - linux - osx - windows jobs: allow_failures: - go: tip - os: windows env: - GO111MODULE=on install: - | if [ "$TRAVIS_OS_NAME" == "linux" ] && [ "$TRAVIS_GO_VERSION" == "1.15.x" ]; then ./scripts/install-ci.sh; fi script: - | if [ "$TRAVIS_OS_NAME" == "linux" ] && [ "$TRAVIS_GO_VERSION" == "1.15.x" ]; then ./scripts/test.sh; else go test ./... -race fi after_success: - | if [ "$TRAVIS_OS_NAME" == "linux" ] && [ "$TRAVIS_GO_VERSION" == "1.15.x" ]; then ./scripts/coverage.sh --coveralls; fi httpretty-0.0.6/CONTRIBUTING.md 0000664 0000000 0000000 00000001115 13725530515 0016021 0 ustar 00root root 0000000 0000000 # Contributing to httpretty ## Bug reports When reporting bugs, please add information about your operating system and Go version used to compile the code. If you can provide a code snippet reproducing the issue, please do so. ## Code Please write code that satisfies [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) before submitting a pull-request. Your code should be properly covered by extensive unit tests. ## Commit messages Please follow the Go [commit messages](https://github.com/golang/go/wiki/CommitMessage) convention when contributing code. httpretty-0.0.6/LICENSE.md 0000664 0000000 0000000 00000002061 13725530515 0015175 0 ustar 00root root 0000000 0000000 MIT License Copyright (c) 2020 Henrique Vicente Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. httpretty-0.0.6/README.md 0000664 0000000 0000000 00000007077 13725530515 0015064 0 ustar 00root root 0000000 0000000 # httpretty [](https://godoc.org/github.com/henvic/httpretty) [](https://travis-ci.org/henvic/httpretty) [](https://coveralls.io/r/henvic/httpretty) [](https://goreportcard.com/report/github.com/henvic/httpretty) [](https://bestpractices.coreinfrastructure.org/projects/3669) 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. [](https://asciinema.org/a/297429) ## Setting up a logger You can define a logger with something like ```go logger := &httpretty.Logger{ Time: true, TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, Colors: true, // erase line if you don't like colors Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, } ``` 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). ### Using on the client-side You can set the transport for the [*net/http.Client](https://golang.org/pkg/net/http/#Client) you are using like this: ```go client := &http.Client{ Transport: logger.RoundTripper(http.DefaultTransport), } // from now on, you can use client.Do, client.Get, etc. to create requests. ``` If you don't care about setting a new client, you can safely replace your existing http.DefaultClient with this: ```go http.DefaultClient.Transport = logger.RoundTripper(http.DefaultClient.Transport) ``` Then httpretty is going to print information about regular requests to your terminal when code such as this is called: ```go if _, err := http.Get("https://www.google.com/"); err != nil { fmt.Fprintf(os.Stderr, "%+v\n", err) os.Exit(1) } ``` However, have in mind you usually want to use a custom *http.Client to control things such as timeout. ## Logging on the server-side You can use the logger quickly to log requests on your server. For example: ```go logger.Middleware(mux) ``` The handler should by a http.Handler. Usually, you want this to be your `http.ServeMux` HTTP entrypoint. For working examples, please see the example directory. ## Filtering You have two ways to filter a request so it isn't printed by the logger. ### httpretty.WithHide You can filter any request by setting a request context before the request reaches `httpretty.RoundTripper`: ```go req = req.WithContext(httpretty.WithHide(ctx)) ``` ### Filter function A second option is to implement ```go type Filter func(req *http.Request) (skip bool, err error) ``` and set it as the filter for your logger. For example: ```go logger.SetFilter(func filteredURIs(req *http.Request) (bool, error) { if req.Method != http.MethodGet { return true, nil } if path := req.URL.Path; path == "/debug" | strings.HasPrefix(path, "/debug/") { return true, nil } return false }) ``` ## Formatters You can define a formatter for any media type by implementing the Formatter interface. We provide a JSONFormatter for convenience (it is not enabled by default). httpretty-0.0.6/binary_test.go 0000664 0000000 0000000 00000002776 13725530515 0016460 0 ustar 00root root 0000000 0000000 package httpretty import ( "bytes" "testing" ) func TestIsBinary(t *testing.T) { testCases := []struct { desc string data []byte binary bool }{ { desc: "Empty", binary: false, }, { desc: "Text", data: []byte("plain text"), binary: false, }, { desc: "More text", data: []byte("plain text\n"), binary: false, }, { desc: "Text with UTF16 Big Endian BOM", data: []byte("\xFE\xFFevil plain text"), binary: false, }, { desc: "Text with UTF16 Little Endian BOM", data: []byte("\xFF\xFEevil plain text"), binary: false, }, { desc: "Text with UTF8 BOM", data: []byte("\xEF\xBB\xBFevil plain text"), binary: false, }, { desc: "Binary", data: []byte{1, 2, 3}, binary: true, }, { desc: "Binary over 512bytes", data: bytes.Repeat([]byte{1, 2, 3, 4, 5, 6, 7, 8}, 65), binary: true, }, { desc: "JPEG image", data: []byte("\xFF\xD8\xFF"), binary: true, }, { desc: "AVI video", data: []byte("RIFF,O\n\x00AVI LISTÀ"), binary: true, }, { desc: "RAR", data: []byte("Rar!\x1A\x07\x00"), binary: true, }, { desc: "PDF", data: []byte("\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7"), binary: true, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { if got := isBinary(tc.data); got != tc.binary { t.Errorf("wanted isBinary(%v) = %v, got %v instead", tc.data, tc.binary, got) } }) } } httpretty-0.0.6/client_test.go 0000664 0000000 0000000 00000141306 13725530515 0016443 0 ustar 00root root 0000000 0000000 package httpretty import ( "bytes" "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "io/ioutil" "log" "mime" "mime/multipart" "net" "net/http" "net/http/httptest" "net/url" "os" "strings" "sync" "testing" "time" ) type helloHandler struct{} func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprintf(w, "Hello, world!") } func TestOutgoing(t *testing.T) { // important: cannot be in parallel because we are capturing os.Stdout ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } // code for capturing stdout was copied from the Go source code file src/testing/run_example.go: // https://github.com/golang/go/blob/ac56baa/src/testing/run_example.go stdout := os.Stdout r, w, err := os.Pipe() if err != nil { panic(err) } os.Stdout = w outC := make(chan string) go func() { var buf strings.Builder _, errcp := io.Copy(&buf, r) r.Close() if errcp != nil { panic(errcp) } outC <- buf.String() }() var want string defer func() { w.Close() os.Stdout = stdout out := <-outC if out != want { t.Errorf("logged HTTP request %s; want %s", out, want) } }() client := &http.Client{ // Only use the default transport (http.DefaultTransport) on TestOutgoing. // Passing nil here = http.DefaultTransport. Transport: logger.RoundTripper(nil), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want = fmt.Sprintf(`* Request to %s > GET / HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, ts.URL, ts.Listener.Addr()) testBody(t, resp.Body, []byte("Hello, world!")) } func outgoingGet(t *testing.T, client *http.Client, ts *httptest.Server, done func()) { defer done() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) } func TestOutgoingConcurrency(t *testing.T) { // don't run in parallel if Race { t.Skip("cannot test because until data race issues are resolved on the standard library https://github.com/golang/go/issues/30597") } ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetFlusher(OnEnd) var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } var wg sync.WaitGroup concurrency := 100 wg.Add(concurrency) for i := 0; i < concurrency; i++ { go outgoingGet(t, client, ts, wg.Done) 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) } wg.Wait() got := buf.String() gotConcurrency := strings.Count(got, "< HTTP/1.1 200 OK") if concurrency != gotConcurrency { t.Errorf("logged %d requests, wanted %d", concurrency, gotConcurrency) } want := fmt.Sprintf(`* Request to %s > GET / HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world!`, ts.URL, ts.Listener.Addr()) if !strings.Contains(got, want) { t.Errorf("Request doesn't contain expected body") } } func TestOutgoingMinimal(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() // only prints the request URI. logger := &Logger{} var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") req.AddCookie(&http.Cookie{ Name: "food", Value: "sorbet", }) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf("* Request to %s\n", ts.URL) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingSanitized(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") req.AddCookie(&http.Cookie{ Name: "food", Value: "sorbet", }) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET / HTTP/1.1 > Host: %s > Cookie: food=████████████████████ > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, ts.URL, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingSkipSanitize(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, SkipSanitize: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") req.AddCookie(&http.Cookie{ Name: "food", Value: "sorbet", }) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET / HTTP/1.1 > Host: %s > Cookie: food=sorbet > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, ts.URL, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingHide(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } req = req.WithContext(WithHide(context.Background())) _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } if buf.Len() != 0 { t.Errorf("request should not be logged, got %v", buf.String()) } want := "" if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func filteredURIs(req *http.Request) (bool, error) { path := req.URL.Path if path == "/filtered" { return true, nil } if path == "/unfiltered" { return false, nil } return false, errors.New("filter error triggered") } func TestOutgoingFilter(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetOutput(ioutil.Discard) logger.SetFilter(filteredURIs) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } testCases := []struct { uri string want string }{ {uri: "filtered"}, {uri: "unfiltered", want: "* Request"}, {uri: "other", want: "filter error triggered"}, } for _, tc := range testCases { t.Run(tc.uri, func(t *testing.T) { var buf bytes.Buffer logger.SetOutput(&buf) _, err := client.Get(fmt.Sprintf("%s/%s", ts.URL, tc.uri)) if err != nil { t.Errorf("cannot create request: %v", err) } if tc.want == "" && buf.Len() != 0 { t.Errorf("wanted input to be filtered, got %v instead", buf.String()) } if !strings.Contains(buf.String(), tc.want) { t.Errorf(`expected input to contain "%v", got %v instead`, tc.want, buf.String()) } }) } } func TestOutgoingFilterPanicked(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetOutput(ioutil.Discard) logger.SetFilter(func(req *http.Request) (bool, error) { panic("evil panic") }) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } var buf bytes.Buffer logger.SetOutput(&buf) _, err := client.Get(ts.URL) if err != nil { t.Errorf("cannot create request: %v", err) } want := fmt.Sprintf(`* cannot filter request: GET %v: panic: evil panic * Request to %v > GET / HTTP/1.1 > Host: %v < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, ts.URL, ts.URL, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf(`expected input to contain "%v", got %v instead`, want, got) } } func TestOutgoingSkipHeader(t *testing.T) { t.Parallel() ts := httptest.NewServer(&jsonHandler{}) defer ts.Close() logger := Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SkipHeader([]string{ "user-agent", "content-type", }) var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s < HTTP/1.1 200 OK < Content-Length: 40 {"result":"Hello, world!","number":3.14} `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingBodyFilter(t *testing.T) { t.Parallel() ts := httptest.NewServer(&jsonHandler{}) defer ts.Close() logger := Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { mediatype, _, _ := mime.ParseMediaType(h.Get("Content-Type")) return mediatype == "application/json", nil }) var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 40 < Content-Type: application/json; charset=utf-8 `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingBodyFilterSoftError(t *testing.T) { t.Parallel() ts := httptest.NewServer(&jsonHandler{}) defer ts.Close() logger := Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { // filter anyway, but print soft error saying something went wrong during the filtering. return true, errors.New("incomplete implementation") }) var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 40 < Content-Type: application/json; charset=utf-8 * error on response body filter: incomplete implementation `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingBodyFilterPanicked(t *testing.T) { t.Parallel() ts := httptest.NewServer(&jsonHandler{}) defer ts.Close() logger := Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { panic("evil panic") }) var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 40 < Content-Type: application/json; charset=utf-8 * panic while filtering body: evil panic {"result":"Hello, world!","number":3.14} `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingWithTimeRequest(t *testing.T) { t.Parallel() ts := httptest.NewServer(&helloHandler{}) defer ts.Close() logger := &Logger{ Time: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } got := buf.String() if !strings.Contains(got, "* Request at ") { t.Error("missing printing start time of request") } if !strings.Contains(got, "* Request took ") { t.Error("missing printing request duration") } } type jsonHandler struct{} func (h jsonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil w.Header().Set("Content-Type", "application/json; charset=utf-8") type res struct { Result string `json:"result"` Number json.Number `json:"number"` } b, err := json.Marshal(res{ Result: "Hello, world!", Number: json.Number("3.14"), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } fmt.Fprint(w, string(b)) } func TestOutgoingFormattedJSON(t *testing.T) { t.Parallel() ts := httptest.NewServer(&jsonHandler{}) defer ts.Close() logger := Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 40 < Content-Type: application/json; charset=utf-8 { "result": "Hello, world!", "number": 3.14 } `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type badJSONHandler struct{} func (h badJSONHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil w.Header().Set("Content-Type", "application/json; charset=utf-8") // wrong content-type on purpose fmt.Fprint(w, `{"bad": }`) } func TestOutgoingBadJSON(t *testing.T) { t.Parallel() ts := httptest.NewServer(&badJSONHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 9 < Content-Type: application/json; charset=utf-8 * body cannot be formatted: invalid character '}' looking for beginning of value {"bad": } `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type panickingFormatter struct{} func (p *panickingFormatter) Match(mediatype string) bool { return true } func (p *panickingFormatter) Format(w io.Writer, src []byte) error { panic("evil formatter") } func TestOutgoingFormatterPanicked(t *testing.T) { t.Parallel() ts := httptest.NewServer(&badJSONHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &panickingFormatter{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 9 < Content-Type: application/json; charset=utf-8 * body cannot be formatted: panic: evil formatter {"bad": } `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type panickingFormatterMatcher struct{} func (p *panickingFormatterMatcher) Match(mediatype string) bool { panic("evil matcher") } func (p *panickingFormatterMatcher) Format(w io.Writer, src []byte) error { return nil } func TestOutgoingFormatterMatcherPanicked(t *testing.T) { t.Parallel() ts := httptest.NewServer(&badJSONHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &panickingFormatterMatcher{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/json", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /json HTTP/1.1 > Host: %s > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Length: 9 < Content-Type: application/json; charset=utf-8 * panic while testing body format: evil matcher {"bad": } `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type formHandler struct{} func (h formHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprint(w, "form received") } func TestOutgoingForm(t *testing.T) { t.Parallel() ts := httptest.NewServer(&formHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } form := url.Values{} form.Add("foo", "bar") form.Add("email", "root@example.com") uri := fmt.Sprintf("%s/form", ts.URL) req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(form.Encode())) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > POST /form HTTP/1.1 > Host: %s email=root%%40example.com&foo=bar < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 form received `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingBinaryBody(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") })) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } b := []byte("RIFF\x00\x00\x00\x00WEBPVP") uri := fmt.Sprintf("%s/convert", ts.URL) req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) req.Header.Add("Content-Type", "image/webp") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > POST /convert HTTP/1.1 > Host: %s > Content-Type: image/webp * body contains binary data < HTTP/1.1 200 OK < Content-Length: 16 < Content-Type: application/pdf * body contains binary data `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingBinaryBodyNoMediatypeHeader(t *testing.T) { t.Parallel() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil w.Header()["Content-Type"] = nil fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") })) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } b := []byte("RIFF\x00\x00\x00\x00WEBPVP") uri := fmt.Sprintf("%s/convert", ts.URL) req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > POST /convert HTTP/1.1 > Host: %s * body contains binary data < HTTP/1.1 200 OK < Content-Length: 16 * body contains binary data `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type longRequestHandler struct{} func (h longRequestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprint(w, "long request received") } func TestOutgoingLongRequest(t *testing.T) { t.Parallel() ts := httptest.NewServer(&longRequestHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/long-request", ts.URL) req, err := http.NewRequest(http.MethodPut, uri, strings.NewReader(petition)) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > PUT /long-request HTTP/1.1 > Host: %s %s < HTTP/1.1 200 OK < Content-Length: 21 < Content-Type: text/plain; charset=utf-8 long request received `, uri, ts.Listener.Addr(), petition) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type longResponseHandler struct{} func (h longResponseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil w.Header().Set("Content-Length", fmt.Sprintf("%d", len(petition))) if r.Method != http.MethodHead { fmt.Fprint(w, petition) } } func TestOutgoingLongResponse(t *testing.T) { t.Parallel() ts := httptest.NewServer(&longResponseHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = int64(len(petition) + 1000) // value larger than the text client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/long-response", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /long-response HTTP/1.1 > Host: %s < HTTP/1.1 200 OK < Content-Length: 9846 < Content-Type: text/plain; charset=utf-8 %s `, uri, ts.Listener.Addr(), petition) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte(petition)) } func TestOutgoingLongResponseHead(t *testing.T) { t.Parallel() ts := httptest.NewServer(&longResponseHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = int64(len(petition) + 1000) // value larger than the text client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/long-response", ts.URL) req, err := http.NewRequest(http.MethodHead, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > HEAD /long-response HTTP/1.1 > Host: %s < HTTP/1.1 200 OK < Content-Length: 9846 `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte{}) } func TestOutgoingTooLongResponse(t *testing.T) { t.Parallel() ts := httptest.NewServer(&longResponseHandler{}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = 5000 // value smaller than the text client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/long-response", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /long-response HTTP/1.1 > Host: %s < HTTP/1.1 200 OK < Content-Length: 9846 < Content-Type: text/plain; charset=utf-8 * body is too long (9846 bytes) to print, skipping (longer than 5000 bytes) `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte(petition)) } type longResponseUnknownLengthHandler struct { repeat int } func (h longResponseUnknownLengthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprint(w, strings.Repeat(petition, h.repeat+1)) } func TestOutgoingLongResponseUnknownLength(t *testing.T) { t.Parallel() testCases := []struct { name string repeat int }{ {name: "short", repeat: 1}, {name: "long", repeat: 100}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ts := httptest.NewServer(&longResponseUnknownLengthHandler{tc.repeat}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, MaxResponseBody: 10000000, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/long-response", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } repeatedBody := strings.Repeat(petition, tc.repeat+1) want := fmt.Sprintf(`* Request to %s > GET /long-response HTTP/1.1 > Host: %s < HTTP/1.1 200 OK < Content-Type: text/plain; charset=utf-8 %s `, uri, ts.Listener.Addr(), repeatedBody) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte(repeatedBody)) }) } } func TestOutgoingLongResponseUnknownLengthTooLong(t *testing.T) { t.Parallel() testCases := []struct { name string repeat int }{ {name: "short", repeat: 1}, {name: "long", repeat: 100}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ts := httptest.NewServer(&longResponseUnknownLengthHandler{tc.repeat}) defer ts.Close() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/long-response", ts.URL) req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET /long-response HTTP/1.1 > Host: %s < HTTP/1.1 200 OK < Content-Type: text/plain; charset=utf-8 * body is too long, skipping (contains more than 4096 bytes) `, uri, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte(strings.Repeat(petition, tc.repeat+1))) }) } } func multipartTestdata(writer *multipart.Writer, body *bytes.Buffer) { params := []struct { name string value string }{ {"author", "Frédéric Bastiat"}, {"title", "Candlemakers' Petition"}, } for _, p := range params { if err := writer.WriteField(p.name, p.value); err != nil { panic(err) } } part, err := writer.CreateFormFile("file", "petition") if err != nil { panic(err) } if _, err = part.Write([]byte(petition)); err != nil { panic(err) } if err = writer.Close(); err != nil { panic(err) } } type multipartHandler struct { t *testing.T } func (h multipartHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := h.t w.Header()["Date"] = nil if err := r.ParseMultipartForm(1000); err != nil { t.Errorf("cannot parse multipart form at server-side: %v", err) } wantAuthor := "Frédéric Bastiat" wantTitle := "Candlemakers' Petition" wantFilename := "petition" gotAuthor := r.Form.Get("author") gotTitle := r.Form.Get("title") if gotAuthor != wantAuthor { t.Errorf("got author %s, wanted %s", gotAuthor, wantAuthor) } if gotTitle != wantTitle { t.Errorf("got author %s, wanted %s", gotTitle, wantTitle) } file, header, err := r.FormFile("file") if err != nil { t.Errorf("server cannot read file form sent over multipart: %v", err) } if header.Filename != wantFilename { t.Errorf("got filename %s, wanted %s", header.Filename, wantFilename) } if header.Size != int64(len(petition)) { t.Errorf("got size %d, wanted %d", header.Size, len(petition)) } b, err := ioutil.ReadAll(file) if err != nil { t.Errorf("server cannot read file sent over multipart: %v", err) } if string(b) != petition { t.Error("server received different text than uploaded") } fmt.Fprint(w, "upload received") } func TestOutgoingMultipartForm(t *testing.T) { t.Parallel() ts := httptest.NewServer(multipartHandler{t}) defer ts.Close() logger := &Logger{ RequestHeader: true, // TODO(henvic): print request body once support for printing out multipart/formdata body is added. ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } uri := fmt.Sprintf("%s/multipart-upload", ts.URL) body := &bytes.Buffer{} writer := multipart.NewWriter(body) multipartTestdata(writer, body) req, err := http.NewRequest(http.MethodPost, uri, body) if err != nil { t.Errorf("cannot create request: %v", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > POST /multipart-upload HTTP/1.1 > Host: %s > Content-Type: %s < HTTP/1.1 200 OK < Content-Length: 15 < Content-Type: text/plain; charset=utf-8 upload received `, uri, ts.Listener.Addr(), writer.FormDataContentType()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingTLS(t *testing.T) { t.Parallel() ts := httptest.NewTLSServer(&helloHandler{}) defer ts.Close() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := ts.Client() client.Transport = logger.RoundTripper(client.Transport) req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Host = "example.com" // overriding the Host header to send req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s > GET / HTTP/1.1 > Host: example.com > User-Agent: Robot/0.1 crawler@example.com * TLS connection using TLS 1.3 / TLS_AES_128_GCM_SHA256 * Server certificate: * subject: O=Acme Co * start date: Thu Jan 1 00:00:00 UTC 1970 * expire date: Sat Jan 29 16:00:00 UTC 2084 * issuer: O=Acme Co * TLS certificate verify ok. < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, ts.URL) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte("Hello, world!")) } func TestOutgoingTLSInsecureSkipVerify(t *testing.T) { t.Parallel() ts := httptest.NewTLSServer(&helloHandler{}) defer ts.Close() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := ts.Client() transport := client.Transport.(*http.Transport) transport.TLSClientConfig.InsecureSkipVerify = true client.Transport = logger.RoundTripper(transport) req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Host = "example.com" // overriding the Host header to send req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } want := fmt.Sprintf(`* Request to %s * Skipping TLS verification: connection is susceptible to man-in-the-middle attacks. > GET / HTTP/1.1 > Host: example.com > User-Agent: Robot/0.1 crawler@example.com * TLS connection using TLS 1.3 / TLS_AES_128_GCM_SHA256 (insecure=true) * Server certificate: * subject: O=Acme Co * start date: Thu Jan 1 00:00:00 UTC 1970 * expire date: Sat Jan 29 16:00:00 UTC 2084 * issuer: O=Acme Co * TLS certificate verify ok. < HTTP/1.1 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, ts.URL) got := buf.String() if got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } testBody(t, resp.Body, []byte("Hello, world!")) } func TestOutgoingTLSInvalidCertificate(t *testing.T) { t.Parallel() ts := httptest.NewTLSServer(&helloHandler{}) ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0) defer ts.Close() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client := &http.Client{ Transport: logger.RoundTripper(newTransport()), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Host = "example.com" // overriding the Host header to send req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) var unknownAuthorityError x509.UnknownAuthorityError if err == nil || !errors.As(err, &unknownAuthorityError) { t.Errorf("cannot connect to the server has unexpected error: %v", err) } want := fmt.Sprintf(`* Request to %s > GET / HTTP/1.1 > Host: example.com > User-Agent: Robot/0.1 crawler@example.com * x509: certificate signed by unknown authority `, ts.URL) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingTLSBadClientCertificate(t *testing.T) { t.Parallel() ts := httptest.NewUnstartedServer(&helloHandler{}) ts.TLS = &tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, } ts.StartTLS() defer ts.Close() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) ts.Config.ErrorLog = log.New(ioutil.Discard, "", 0) client := ts.Client() cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") if err != nil { panic(err) } cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) if err != nil { t.Errorf("failed to parse certificate for copying Leaf field") } transport := client.Transport.(*http.Transport) transport.TLSClientConfig.Certificates = []tls.Certificate{ cert, } client.Transport = logger.RoundTripper(transport) req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Host = "example.com" // overriding the Host header to send req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err == nil || !strings.Contains(err.Error(), "bad certificate") { t.Errorf("got: %v, expected bad certificate error message", err) } want := fmt.Sprintf(`* Request to %s * Client certificate: * subject: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL * start date: Sat Jan 25 20:12:36 UTC 2020 * expire date: Mon Jan 1 20:12:36 UTC 2120 * issuer: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL > GET / HTTP/1.1 > Host: example.com > User-Agent: Robot/0.1 crawler@example.com * remote error: tls: bad certificate `, ts.URL) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingHTTP2MutualTLS(t *testing.T) { t.Parallel() caCert, err := ioutil.ReadFile("testdata/cert.pem") if err != nil { panic(err) } clientCert, err := ioutil.ReadFile("testdata/cert-client.pem") if err != nil { panic(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(clientCert) tlsConfig := &tls.Config{ ClientCAs: caCertPool, ClientAuth: tls.RequireAndVerifyClientCert, } // NOTE(henvic): Using httptest directly turned out complicated. // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go server := &http.Server{ TLSConfig: tlsConfig, Handler: &helloHandler{}, } listener, err := netListener() if err != nil { panic(fmt.Sprintf("failed to listen on a port: %v", err)) } defer listener.Close() go func() { // Certificate generated with // $ openssl req -x509 -newkey rsa:2048 \ // -new -nodes -sha256 \ // -days 36500 \ // -out cert.pem \ // -keyout key.pem \ // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { t.Errorf("server exit with unexpected error: %v", errcp) } }() defer server.Shutdown(context.Background()) // Certificate generated with // $ openssl req -newkey rsa:2048 \ // -new -nodes -x509 \ // -days 36500 \ // -out cert-client.pem \ // -keyout key-client.pem \ // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") if err != nil { t.Errorf("failed to load X509 key pair: %v", err) } cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) if err != nil { t.Errorf("failed to parse certificate for copying Leaf field") } // Create a HTTPS client and supply the created CA pool and certificate clientTLSConfig := &tls.Config{ RootCAs: caCertPool, Certificates: []tls.Certificate{cert}, } transport := newTransport() transport.TLSClientConfig = clientTLSConfig client := &http.Client{ Transport: transport, } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client.Transport = logger.RoundTripper(client.Transport) _, port, err := net.SplitHostPort(listener.Addr().String()) if err != nil { panic(err) } var host = fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) resp, err := client.Get(host) if err != nil { t.Errorf("cannot create request: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) want := fmt.Sprintf(`* Request to %s * Client certificate: * subject: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL * start date: Sat Jan 25 20:12:36 UTC 2020 * expire date: Mon Jan 1 20:12:36 UTC 2120 * issuer: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL > GET /mutual-tls-test HTTP/1.1 > Host: localhost:%s * TLS connection using TLS 1.3 / TLS_AES_128_GCM_SHA256 * ALPN: h2 accepted * Server certificate: * subject: CN=localhost,OU=Cloud,O=Plifk,L=Carmel-by-the-Sea,ST=California,C=US * start date: Wed Aug 12 22:20:45 UTC 2020 * expire date: Fri Jul 19 22:20:45 UTC 2120 * issuer: CN=localhost,OU=Cloud,O=Plifk,L=Carmel-by-the-Sea,ST=California,C=US * TLS certificate verify ok. < HTTP/2.0 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, host, port) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestOutgoingHTTP2MutualTLSNoSafetyLogging(t *testing.T) { t.Parallel() caCert, err := ioutil.ReadFile("testdata/cert.pem") if err != nil { panic(err) } clientCert, err := ioutil.ReadFile("testdata/cert-client.pem") if err != nil { panic(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(clientCert) tlsConfig := &tls.Config{ ClientCAs: caCertPool, ClientAuth: tls.RequireAndVerifyClientCert, } // NOTE(henvic): Using httptest directly turned out complicated. // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go server := &http.Server{ TLSConfig: tlsConfig, Handler: &helloHandler{}, } listener, err := netListener() if err != nil { panic(fmt.Sprintf("failed to listen on a port: %v", err)) } defer listener.Close() go func() { // Certificate generated with // $ openssl req -x509 -newkey rsa:2048 \ // -new -nodes -sha256 \ // -days 36500 \ // -out cert.pem \ // -keyout key.pem \ // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { t.Errorf("server exit with unexpected error: %v", errcp) } }() defer server.Shutdown(context.Background()) // Certificate generated with // $ openssl req -newkey rsa:2048 \ // -new -nodes -x509 \ // -days 36500 \ // -out cert-client.pem \ // -keyout key-client.pem \ // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") if err != nil { t.Errorf("failed to load X509 key pair: %v", err) } cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) if err != nil { t.Errorf("failed to parse certificate for copying Leaf field") } // Create a HTTPS client and supply the created CA pool and certificate clientTLSConfig := &tls.Config{ RootCAs: caCertPool, Certificates: []tls.Certificate{cert}, } transport := newTransport() transport.TLSClientConfig = clientTLSConfig client := &http.Client{ Transport: transport, } logger := &Logger{ // TLS must be false RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) client.Transport = logger.RoundTripper(client.Transport) _, port, err := net.SplitHostPort(listener.Addr().String()) if err != nil { panic(err) } var host = fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) resp, err := client.Get(host) if err != nil { t.Errorf("cannot create request: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) want := fmt.Sprintf(`* Request to %s > GET /mutual-tls-test HTTP/1.1 > Host: localhost:%s < HTTP/2.0 200 OK < Content-Length: 13 < Content-Type: text/plain; charset=utf-8 Hello, world! `, host, port) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } // netListener is similar to httptest.newlocalListener() and listens locally in a random port. // See https://github.com/golang/go/blob/5375c71289917ac7b25c6fa4bb0f4fa17be19a07/src/net/http/httptest/server.go#L60-L75 func netListener() (listener net.Listener, err error) { listener, err = net.Listen("tcp", "127.0.0.1:0") if err != nil { return net.Listen("tcp6", "[::1]:0") } return } httpretty-0.0.6/example/ 0000775 0000000 0000000 00000000000 13725530515 0015225 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/example/client/ 0000775 0000000 0000000 00000000000 13725530515 0016503 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/example/client/main.go 0000664 0000000 0000000 00000001150 13725530515 0017753 0 ustar 00root root 0000000 0000000 package main import ( "fmt" "net/http" "os" "github.com/henvic/httpretty" ) func main() { logger := &httpretty.Logger{ Time: true, TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, Colors: true, // erase line if you don't like colors Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, } client := &http.Client{ Transport: logger.RoundTripper(http.DefaultTransport), } if _, err := client.Get("https://www.google.com/"); err != nil { fmt.Fprintf(os.Stderr, "%+v\n", err) os.Exit(1) } } httpretty-0.0.6/example/httprepl/ 0000775 0000000 0000000 00000000000 13725530515 0017067 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/example/httprepl/main.go 0000664 0000000 0000000 00000003446 13725530515 0020351 0 ustar 00root root 0000000 0000000 package main import ( "bufio" "fmt" "net/http" "net/url" "os" "runtime" "strings" "github.com/henvic/httpretty" ) func main() { logger := &httpretty.Logger{ Time: true, TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, Colors: true, // erase line if you don't like colors Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, } client := &http.Client{ Transport: logger.RoundTripper(http.DefaultTransport), } fmt.Print("httprepl is a small HTTP client REPL (read-eval-print-loop) program example\n\n") help() reader := bufio.NewReader(os.Stdin) for { fmt.Print("$ ") readEvalPrint(reader, client) } } func readEvalPrint(reader *bufio.Reader, client *http.Client) { s, err := reader.ReadString('\n') if err != nil { fmt.Fprintf(os.Stderr, "cannot read stdin: %v\n", err) os.Exit(1) } if runtime.GOOS == "windows" { s = strings.TrimRight(s, "\r\n") } else { s = strings.TrimRight(s, "\n") } s = strings.TrimSpace(s) switch { case s == "exit": os.Exit(0) case s == "help": help() return case s == "": return case s == "get": fmt.Fprintln(os.Stderr, "missing address") case !strings.HasPrefix(s, "get "): fmt.Fprint(os.Stderr, "invalid command\n\n") return } s = strings.TrimPrefix(s, "get ") uri, err := url.Parse(s) if err == nil && uri.Scheme == "" { uri.Scheme = "http" s = uri.String() } // we just ignore the request contents but you can see it printed thanks to the logger. if _, err := client.Get(s); err != nil { fmt.Fprintf(os.Stderr, "%+v\n\n", err) } fmt.Println() } func help() { fmt.Print(`Commands available: get
URL to get. Example: "get www.google.com" help This command list exit Quit the application `) } httpretty-0.0.6/example/server/ 0000775 0000000 0000000 00000000000 13725530515 0016533 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/example/server/main.go 0000664 0000000 0000000 00000001325 13725530515 0020007 0 ustar 00root root 0000000 0000000 package main import ( "fmt" "net/http" "os" "github.com/henvic/httpretty" ) func main() { logger := &httpretty.Logger{ Time: true, TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, Colors: true, // erase line if you don't like colors } addr := ":8090" fmt.Printf("Open http://localhost%s in the browser.\n", addr) if err := http.ListenAndServe(":8090", logger.Middleware(helloHandler{})); err != http.ErrServerClosed { fmt.Fprintln(os.Stderr, err) } } type helloHandler struct{} func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprintf(w, "Hello, world!") } httpretty-0.0.6/go.mod 0000664 0000000 0000000 00000000054 13725530515 0014677 0 ustar 00root root 0000000 0000000 module github.com/henvic/httpretty go 1.13 httpretty-0.0.6/httpretty.go 0000664 0000000 0000000 00000026256 13725530515 0016203 0 ustar 00root root 0000000 0000000 // Package httpretty prints your HTTP requests pretty on your terminal screen. // You can use this package both on the client-side and on the server-side. // // This package provides a better way to view HTTP traffic without httputil // DumpRequest, DumpRequestOut, and DumpResponse heavy debugging functions. // // You can use the logger quickly to log requests you are opening. For example: // package main // // import ( // "fmt" // "net/http" // "os" // // "github.com/henvic/httpretty" // ) // // func main() { // logger := &httpretty.Logger{ // Time: true, // TLS: true, // RequestHeader: true, // RequestBody: true, // ResponseHeader: true, // ResponseBody: true, // Colors: true, // Formatters: []httpretty.Formatter{&httpretty.JSONFormatter{}}, // } // // http.DefaultClient.Transport = logger.RoundTripper(http.DefaultClient.Transport) // tip: you can use it on any *http.Client // // if _, err := http.Get("https://www.google.com/"); err != nil { // fmt.Fprintf(os.Stderr, "%+v\n", err) // os.Exit(1) // } // } // // If you pass nil to the logger.RoundTripper it is going to fallback to http.DefaultTransport. // // You can use the logger quickly to log requests on your server. For example: // logger := &httpretty.Logger{ // Time: true, // TLS: true, // RequestHeader: true, // RequestBody: true, // ResponseHeader: true, // ResponseBody: true, // } // // logger.Middleware(handler) // // Note: server logs don't include response headers set by the server. // Client logs don't include request headers set by the HTTP client. package httpretty import ( "bytes" "context" "crypto/tls" "encoding/json" "errors" "io" "net/http" "net/textproto" "os" "sync" "github.com/henvic/httpretty/internal/color" ) // Formatter can be used to format body. // // If the Format function returns an error, the content is printed in verbatim after a warning. // Match receives a media type from the Content-Type field. The body is formatted if it returns true. type Formatter interface { Match(mediatype string) bool Format(w io.Writer, src []byte) error } // WithHide can be used to protect a request from being exposed. func WithHide(ctx context.Context) context.Context { return context.WithValue(ctx, contextHide{}, struct{}{}) } // Logger provides a way for you to print client and server-side information about your HTTP traffic. type Logger struct { // SkipRequestInfo avoids printing a line showing the request URI on all requests plus a line // containing the remote address on server-side requests. SkipRequestInfo bool // Time the request began and its duration. Time bool // TLS information, such as certificates and ciphers. // BUG(henvic): Currently, the TLS information prints after the response header, although it // should be printed before the request header. TLS bool // RequestHeader set by the client or received from the server. RequestHeader bool // RequestBody sent by the client or received by the server. RequestBody bool // ResponseHeader received by the client or set by the HTTP handlers. ResponseHeader bool // ResponseBody received by the client or set by the server. ResponseBody bool // SkipSanitize bypasses sanitizing headers containing credentials (such as Authorization). SkipSanitize bool // Colors set ANSI escape codes that terminals use to print text in different colors. Colors bool // Formatters for the request and response bodies. // No standard formatters are used. You need to add what you want to use explicitly. // We provide a JSONFormatter for convenience (add it manually). Formatters []Formatter // MaxRequestBody the logger can print. // If value is not set and Content-Length is not sent, 4096 bytes is considered. MaxRequestBody int64 // MaxResponseBody the logger can print. // If value is not set and Content-Length is not sent, 4096 bytes is considered. MaxResponseBody int64 mu sync.Mutex // ensures atomic writes; protects the following fields w io.Writer filter Filter skipHeader map[string]struct{} bodyFilter BodyFilter flusher Flusher } // Filter allows you to skip requests. // // If an error happens and you want to log it, you can pass a not-null error value. type Filter func(req *http.Request) (skip bool, err error) // BodyFilter allows you to skip printing a HTTP body based on its associated Header. // // It can be used for omitting HTTP Request and Response bodies. // You can filter by checking properties such as Content-Type or Content-Length. // // On a HTTP server, this function is called even when no body is present due to // http.Request always carrying a non-nil value. type BodyFilter func(h http.Header) (skip bool, err error) // Flusher defines how logger prints requests. type Flusher int // Logger can print without flushing, when they are available, or when the request is done. const ( // NoBuffer strategy prints anything immediately, without buffering. // It has the issue of mingling concurrent requests in unpredictable ways. NoBuffer Flusher = iota // OnReady buffers and prints each step of the request or response (header, body) whenever they are ready. // It reduces mingling caused by mingling but does not give any ordering guarantee, so responses can still be out of order. OnReady // OnEnd buffers the whole request and flushes it once, in the end. OnEnd ) // SetFilter allows you to set a function to skip requests. // Pass nil to remove the filter. This method is concurrency safe. func (l *Logger) SetFilter(f Filter) { l.mu.Lock() defer l.mu.Unlock() l.filter = f } // SkipHeader allows you to skip printing specific headers. // This method is concurrency safe. func (l *Logger) SkipHeader(headers []string) { l.mu.Lock() defer l.mu.Unlock() m := map[string]struct{}{} for _, h := range headers { m[textproto.CanonicalMIMEHeaderKey(h)] = struct{}{} } l.skipHeader = m } // SetBodyFilter allows you to set a function to skip printing a body. // Pass nil to remove the body filter. This method is concurrency safe. func (l *Logger) SetBodyFilter(f BodyFilter) { l.mu.Lock() defer l.mu.Unlock() l.bodyFilter = f } // SetOutput sets the output destination for the logger. func (l *Logger) SetOutput(w io.Writer) { l.mu.Lock() defer l.mu.Unlock() l.w = w } // SetFlusher sets the flush strategy for the logger. func (l *Logger) SetFlusher(f Flusher) { l.mu.Lock() defer l.mu.Unlock() l.flusher = f } func (l *Logger) getWriter() io.Writer { if l.w == nil { return os.Stdout } return l.w } func (l *Logger) getFilter() Filter { l.mu.Lock() f := l.filter defer l.mu.Unlock() return f } func (l *Logger) getBodyFilter() BodyFilter { l.mu.Lock() f := l.bodyFilter defer l.mu.Unlock() return f } func (l *Logger) cloneSkipHeader() map[string]struct{} { l.mu.Lock() skipped := l.skipHeader l.mu.Unlock() m := map[string]struct{}{} for h := range skipped { m[h] = struct{}{} } return m } type contextHide struct{} type roundTripper struct { logger *Logger rt http.RoundTripper } // RoundTripper returns a RoundTripper that uses the logger. func (l *Logger) RoundTripper(rt http.RoundTripper) http.RoundTripper { return roundTripper{ logger: l, rt: rt, } } // RoundTrip implements the http.RoundTrip interface. func (r roundTripper) RoundTrip(req *http.Request) (resp *http.Response, err error) { tripper := r.rt if tripper == nil { // BUG(henvic): net/http data race condition when the client // does concurrent requests using the very same HTTP transport. // See Go standard library issue https://golang.org/issue/30597 tripper = http.RoundTripper(http.DefaultTransport) } l := r.logger p := newPrinter(l) defer p.flush() if hide := req.Context().Value(contextHide{}); hide != nil || p.checkFilter(req) { return tripper.RoundTrip(req) } var tlsClientConfig *tls.Config if l.Time { defer p.printTimeRequest()() } if !l.SkipRequestInfo { p.printRequestInfo(req) } if transport, ok := tripper.(*http.Transport); ok && transport.TLSClientConfig != nil { tlsClientConfig = transport.TLSClientConfig if tlsClientConfig.InsecureSkipVerify { p.printf("* Skipping TLS verification: %s\n", p.format(color.FgRed, "connection is susceptible to man-in-the-middle attacks.")) } } if l.TLS && tlsClientConfig != nil { // please remember http.Request.TLS is ignored by the HTTP client. p.printOutgoingClientTLS(tlsClientConfig) } p.printRequest(req) defer func() { if err != nil { p.printf("* %s\n", p.format(color.FgRed, err.Error())) if resp == nil { return } } if l.TLS { p.printTLSInfo(resp.TLS, false) p.printTLSServer(req.Host, resp.TLS) } p.printResponse(resp) }() return tripper.RoundTrip(req) } // Middleware for logging incoming requests to a HTTP server. func (l *Logger) Middleware(next http.Handler) http.Handler { return httpHandler{ logger: l, next: next, } } type httpHandler struct { logger *Logger next http.Handler } // ServeHTTP is a middleware for logging incoming requests to a HTTP server. func (h httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { l := h.logger p := newPrinter(l) defer p.flush() if hide := req.Context().Value(contextHide{}); hide != nil || p.checkFilter(req) { h.next.ServeHTTP(w, req) return } if p.logger.Time { defer p.printTimeRequest()() } if !p.logger.SkipRequestInfo { p.printRequestInfo(req) } if p.logger.TLS { p.printTLSInfo(req.TLS, true) p.printIncomingClientTLS(req.TLS) } p.printRequest(req) rec := &responseRecorder{ ResponseWriter: w, statusCode: http.StatusOK, maxReadableBody: l.MaxResponseBody, buf: &bytes.Buffer{}, } defer p.printServerResponse(req, rec) h.next.ServeHTTP(rec, req) } // PrintRequest prints a request, even when WithHide is used to hide it. // // It doesn't log TLS connection details or request duration. func (l *Logger) PrintRequest(req *http.Request) { var p = printer{logger: l} if skip := p.checkFilter(req); skip { return } p.printRequest(req) } // PrintResponse prints a response. func (l *Logger) PrintResponse(resp *http.Response) { var p = printer{logger: l} p.printResponse(resp) } // JSONFormatter helps you read unreadable JSON documents. // // github.com/tidwall/pretty could be used to add colors to it. // However, it would add an external dependency. If you want, you can define // your own formatter using it or anything else. See Formatter. type JSONFormatter struct{} // Match JSON media type. func (j *JSONFormatter) Match(mediatype string) bool { return mediatype == "application/json" } // Format JSON content. func (j *JSONFormatter) Format(w io.Writer, src []byte) error { if !json.Valid(src) { // We want to get the error of json.checkValid, not unmarshal it. // The happy path has been optimized, maybe prematurely. if err := json.Unmarshal(src, &json.RawMessage{}); err != nil { return err } } // avoiding allocation as we use *bytes.Buffer to store the formatted body before printing dst, ok := w.(*bytes.Buffer) if !ok { // mitigating panic to avoid upsetting anyone who uses this directly return errors.New("underlying writer for JSONFormatter must be *bytes.Buffer") } return json.Indent(dst, src, "", " ") } httpretty-0.0.6/httpretty_test.go 0000664 0000000 0000000 00000014023 13725530515 0017227 0 ustar 00root root 0000000 0000000 package httpretty import ( "bytes" "io" "io/ioutil" "net" "net/http" "net/url" "os" "reflect" "testing" "time" ) func TestPrintRequest(t *testing.T) { t.Parallel() var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) if err != nil { panic(err) } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.PrintRequest(req) want := `> POST / HTTP/1.1 > Host: wxww.example.com ` if got := buf.String(); got != want { t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) } } func TestPrintRequestWithColors(t *testing.T) { t.Parallel() var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) if err != nil { panic(err) } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, Colors: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.PrintRequest(req) want := "> \x1b[34;1mPOST\x1b[0m \x1b[33m/\x1b[0m \x1b[34mHTTP/1.1\x1b[0m" + "\n> \x1b[34;1mHost\x1b[0m\x1b[31m:\x1b[0m \x1b[33mwxww.example.com\x1b[0m\n\n" if got := buf.String(); got != want { t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) } } func TestEncodingQueryStringParams(t *testing.T) { // Regression test for verifying query string parameters are being encoded correctly when printing with colors. // Issue reported by @mislav in https://github.com/henvic/httpretty/issues/9. t.Parallel() qs := url.Values{} qs.Set("a", "b") qs.Set("i", "j") qs.Set("x", "y") qs.Set("z", "+=") qs.Set("var", "foo&bar") u := url.URL{ Scheme: "http", Host: "www.example.com", Path: "/mypath", RawQuery: qs.Encode(), } var req, err = http.NewRequest(http.MethodPost, u.String(), nil) if err != nil { panic(err) } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, Colors: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.PrintRequest(req) 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" + "\n> \x1b[34;1mHost\x1b[0m\x1b[31m:\x1b[0m \x1b[33mwww.example.com\x1b[0m\n\n" if got := buf.String(); got != want { t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) } } func TestEncodingQueryStringParamsNoColors(t *testing.T) { t.Parallel() qs := url.Values{} qs.Set("a", "b") qs.Set("i", "j") qs.Set("x", "y") qs.Set("z", "+=") qs.Set("var", "foo&bar") u := url.URL{ Scheme: "http", Host: "www.example.com", Path: "/mypath", RawQuery: qs.Encode(), } var req, err = http.NewRequest(http.MethodPost, u.String(), nil) if err != nil { panic(err) } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.PrintRequest(req) want := `> POST /mypath?a=b&i=j&var=foo%26bar&x=y&z=%2B%3D HTTP/1.1 > Host: www.example.com ` if got := buf.String(); got != want { t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) } } func TestPrintRequestFiltered(t *testing.T) { t.Parallel() var req, err = http.NewRequest(http.MethodPost, "http://wxww.example.com/", nil) if err != nil { panic(err) } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SetFilter(func(req *http.Request) (skip bool, err error) { return true, nil }) logger.PrintRequest(req) if got := buf.Len(); got != 0 { t.Errorf("got %v from logger, wanted nothing (everything should be filtered)", got) } } func TestPrintRequestNil(t *testing.T) { t.Parallel() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.PrintRequest(nil) want := "> error: null request\n" if got := buf.String(); got != want { t.Errorf("PrintRequest(req) = %v, wanted %v", got, want) } } func TestPrintResponseNil(t *testing.T) { t.Parallel() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.PrintResponse(nil) want := "< error: null response\n" if got := buf.String(); got != want { t.Errorf("PrintResponse(req) = %v, wanted %v", got, want) } } func testBody(t *testing.T, r io.Reader, want []byte) { t.Helper() got, err := ioutil.ReadAll(r) if err != nil { t.Errorf("expected no error reading response body, got %v instead", err) } if !reflect.DeepEqual(got, want) { t.Errorf(`got body = %v, wanted %v`, string(got), string(want)) } } func TestJSONFormatterWriterError(t *testing.T) { // verifies if function doesn't panic if passed writer isn't *bytes.Buffer f := &JSONFormatter{} want := "underlying writer for JSONFormatter must be *bytes.Buffer" if err := f.Format(os.Stdout, []byte(`{}`)); err == nil || err.Error() != want { t.Errorf("got format error = %v, wanted %v", err, want) } } // newTransport creates a new HTTP Transport. // // BUG(henvic): this function is mostly used at this moment because of a data race condition on the standard library. // See https://github.com/golang/go/issues/30597 for details. func newTransport() *http.Transport { // values copied from Go 1.13.7 http.DefaultTransport variable. return &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } } httpretty-0.0.6/internal/ 0000775 0000000 0000000 00000000000 13725530515 0015406 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/internal/color/ 0000775 0000000 0000000 00000000000 13725530515 0016524 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/internal/color/color.go 0000664 0000000 0000000 00000005731 13725530515 0020177 0 ustar 00root root 0000000 0000000 // Package color can be used to add color to your terminal using ANSI escape code (or sequences). // // See https://en.wikipedia.org/wiki/ANSI_escape_code // Copy modified from https://github.com/fatih/color // Copyright 2013 Fatih Arslan package color import ( "fmt" "strconv" "strings" ) // Attribute defines a single SGR (Select Graphic Rendition) code. type Attribute int // Base attributes const ( Reset Attribute = iota Bold Faint Italic Underline BlinkSlow BlinkRapid ReverseVideo Concealed CrossedOut ) // Foreground text colors const ( FgBlack Attribute = iota + 30 FgRed FgGreen FgYellow FgBlue FgMagenta FgCyan FgWhite ) // Foreground Hi-Intensity text colors const ( FgHiBlack Attribute = iota + 90 FgHiRed FgHiGreen FgHiYellow FgHiBlue FgHiMagenta FgHiCyan FgHiWhite ) // Background text colors const ( BgBlack Attribute = iota + 40 BgRed BgGreen BgYellow BgBlue BgMagenta BgCyan BgWhite ) // Background Hi-Intensity text colors const ( BgHiBlack Attribute = iota + 100 BgHiRed BgHiGreen BgHiYellow BgHiBlue BgHiMagenta BgHiCyan BgHiWhite ) const ( escape = "\x1b" unescape = "\\x1b" ) // Format text for terminal. // You can pass an arbitrary number of Attribute or []Attribute followed by any other values, // that can either be a string or something else (that is converted to string using fmt.Sprint). func Format(s ...interface{}) string { if len(s) == 0 { return "" } params := []Attribute{} in := -1 for i, v := range s { switch vt := v.(type) { case []Attribute: if in == -1 { params = append(params, vt...) } else { s[i] = printExtraColorAttribute(v) } case Attribute: if in == -1 { params = append(params, vt) } else { s[i] = printExtraColorAttribute(v) } default: if in == -1 { in = i } } } if in == -1 || len(s[in:]) == 0 { return "" } return wrap(params, fmt.Sprint(s[in:]...)) } func printExtraColorAttribute(v interface{}) string { return fmt.Sprintf("(EXTRA color.Attribute=%v)", v) } // StripAttributes from input arguments and return unformatted text. func StripAttributes(s ...interface{}) (raw string) { in := -1 for i, v := range s { switch v.(type) { case []Attribute, Attribute: if in != -1 { s[i] = printExtraColorAttribute(v) } default: if in == -1 { in = i } } } if in == -1 { in = 0 } return fmt.Sprint(s[in:]...) } // Escape text for terminal. func Escape(s string) string { return strings.Replace(s, escape, unescape, -1) } // sequence returns a formated SGR sequence to be plugged into a "\x1b[...m" // an example output might be: "1;36" -> bold cyan. func sequence(params []Attribute) string { format := make([]string, len(params)) for i, v := range params { format[i] = strconv.Itoa(int(v)) } return strings.Join(format, ";") } // wrap the s string with the colors attributes. func wrap(params []Attribute, s string) string { return fmt.Sprintf("%s[%sm%s%s[%dm", escape, sequence(params), s, escape, Reset) } httpretty-0.0.6/internal/color/color_test.go 0000664 0000000 0000000 00000006766 13725530515 0021247 0 ustar 00root root 0000000 0000000 package color import ( "reflect" "testing" ) func TestFormat(t *testing.T) { want := "\x1b[102;95mHello World\x1b[0m" got := Format(BgHiGreen, FgHiMagenta, "Hello World") if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestMalformedFormat(t *testing.T) { want := "\x1b[102mHello World(EXTRA color.Attribute=95)\x1b[0m" got := Format(BgHiGreen, "Hello World", FgHiMagenta) if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestMalformedSliceFormat(t *testing.T) { want := "\x1b[102mHello World(EXTRA color.Attribute=[95 41])\x1b[0m" got := Format(BgHiGreen, "Hello World", []Attribute{FgHiMagenta, BgRed}) if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestFormatSlice(t *testing.T) { format := []Attribute{BgHiGreen, FgHiMagenta} want := "\x1b[102;95mHello World\x1b[0m" got := Format(format, "Hello World") if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestEmpty(t *testing.T) { want := "" got := Format() if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestEmptyColorString(t *testing.T) { want := "" got := Format(BgBlack) if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestNoFormat(t *testing.T) { want := "\x1b[mHello World\x1b[0m" got := Format("Hello World") if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestFormatStartingWithNumber(t *testing.T) { want := "\x1b[102;95m100 forks\x1b[0m" number := 100 if reflect.TypeOf(number).String() != "int" { t.Errorf("Must be integer; not a similar like Attribute") } got := Format(BgHiGreen, FgHiMagenta, number, " forks") if got != want { t.Errorf("Expecting %s, got '%s'\n", want, got) } } func TestFormatCtrlChar(t *testing.T) { if want, got := "\x1b[ma%b\x1b[0m", Format("a%b"); got != want { t.Errorf(`expected Format(a%%b) to be %q, got %q instead`, want, got) } if want, got := "\\x1b[34;46ma%b\\x1b[0m", Escape(Format(FgBlue, BgCyan, "a%b")); got != want { t.Errorf(`expected escaped formatted a%%b to be %q, got %q instead`, want, got) } } func TestEscape(t *testing.T) { unescaped := "\x1b[32mGreen" escaped := "\\x1b[32mGreen" got := Escape(unescaped) if got != escaped { t.Errorf("Expecting %s, got '%s'\n", escaped, got) } } func TestStripAttributes(t *testing.T) { want := "this is a regular string" got := StripAttributes(FgCyan, []Attribute{FgBlack}, "this is a regular string") if got != want { t.Errorf("StripAttributes(input) = %s, wanted %s", got, want) } } func TestStripAttributesEmpty(t *testing.T) { if got := StripAttributes(); got != "" { t.Errorf("StripAttributes() should work") } } func TestStripAttributesFirstParam(t *testing.T) { want := "foo (EXTRA color.Attribute=32)" got := StripAttributes("foo ", FgGreen) if got != want { t.Errorf(`expected StripAttributes = %v, got %v instead`, want, got) } } func TestStripAttributesSame(t *testing.T) { want := "this is a regular string" got := StripAttributes(want) if got != want { t.Errorf("StripAttributes(%s) = %s, wanted %s", want, got, want) } } func TestStripAttributesWithExtraColorAttribute(t *testing.T) { want := "this is a regular string (EXTRA color.Attribute=91) with an invalid color Attribute field" got := StripAttributes(BgCyan, []Attribute{FgBlack}, "this is a regular string ", FgHiRed, " with an invalid color Attribute field") if got != want { t.Errorf("StripAttributes(input) = %s, wanted %s", got, want) } } httpretty-0.0.6/internal/header/ 0000775 0000000 0000000 00000000000 13725530515 0016636 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/internal/header/header.go 0000664 0000000 0000000 00000004777 13725530515 0020434 0 ustar 00root root 0000000 0000000 // Package header can be used to sanitize HTTP request and response headers. package header import ( "fmt" "net/http" "strings" ) // Sanitize list of headers. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ can be consulted for header syntax. func Sanitize(sanitizers map[string]SanitizeHeaderFunc, headers http.Header) http.Header { var redacted = http.Header{} for k, values := range headers { if s, ok := sanitizers[http.CanonicalHeaderKey(k)]; ok { redacted[k] = sanitize(s, values) continue } redacted[k] = values } return redacted } func sanitize(s SanitizeHeaderFunc, values []string) []string { var redacted = []string{} for _, v := range values { redacted = append(redacted, s(v)) } return redacted } // DefaultSanitizers contains a list of sanitizers to be used for common headers. var DefaultSanitizers = map[string]SanitizeHeaderFunc{ "Authorization": AuthorizationSanitizer, "Set-Cookie": SetCookieSanitizer, "Cookie": CookieSanitizer, "Proxy-Authorization": AuthorizationSanitizer, } // SanitizeHeaderFunc implements sanitization for a header value. type SanitizeHeaderFunc func(string) string // AuthorizationSanitizer is used to sanitize Authorization and Proxy-Authorization headers. func AuthorizationSanitizer(unsafe string) string { if unsafe == "" { return "" } directives := strings.SplitN(unsafe, " ", 2) l := 0 if len(directives) > 1 { l = len(directives[1]) } if l == 0 { return directives[0] } return directives[0] + " " + redact(l) } // SetCookieSanitizer is used to sanitize Set-Cookie header. func SetCookieSanitizer(unsafe string) string { directives := strings.SplitN(unsafe, ";", 2) cookie := strings.SplitN(directives[0], "=", 2) l := 0 if len(cookie) > 1 { l = len(cookie[1]) } if len(directives) == 2 { return fmt.Sprintf("%s=%s; %s", cookie[0], redact(l), strings.TrimPrefix(directives[1], " ")) } return fmt.Sprintf("%s=%s", cookie[0], redact(l)) } // CookieSanitizer is used to sanitize Cookie header. func CookieSanitizer(unsafe string) string { cookies := strings.Split(unsafe, ";") var list []string for _, unsafeCookie := range cookies { cookie := strings.SplitN(unsafeCookie, "=", 2) l := 0 if len(cookie) > 1 { l = len(cookie[1]) } list = append(list, fmt.Sprintf("%s=%s", cookie[0], redact(l))) } return strings.Join(list, "; ") } func redact(count int) string { if count == 0 { return "" } return "████████████████████" } httpretty-0.0.6/internal/header/header_test.go 0000664 0000000 0000000 00000003572 13725530515 0021463 0 ustar 00root root 0000000 0000000 package header import ( "net/http" "reflect" "testing" ) func TestSanitize(t *testing.T) { var headers = http.Header{} // no need to test request and response headers sanitization separately headers.Set("Accept", "*/*") headers.Set("User-Agent", "curl/7.54.0") headers.Add("Cookie", "abcd=secret1") headers.Add("Cookie", "xyz=secret2") headers.Add("Set-Cookie", "session_id=secret3") headers.Add("Set-Cookie", "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly") headers.Add("Authorization", "Bearer foo") headers.Add("Proxy-Authorization", "Basic Zm9vQGV4YW1wbGUuY29tOmJhcg==") headers.Set("Content-Type", "application/x-www-form-urlencoded") headers.Set("Content-Length", "3") var got = Sanitize(DefaultSanitizers, headers) if len(headers) != len(got) { t.Errorf("Expected length of sanitized headers (%d) to be equal to length of original headers (%d)", len(got), len(headers)) } want := http.Header{ "Accept": []string{"*/*"}, "User-Agent": []string{"curl/7.54.0"}, "Cookie": []string{"abcd=████████████████████", "xyz=████████████████████"}, "Set-Cookie": []string{"session_id=████████████████████", "id=████████████████████; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"}, "Authorization": []string{"Bearer ████████████████████"}, "Proxy-Authorization": []string{"Basic ████████████████████"}, "Content-Type": []string{"application/x-www-form-urlencoded"}, "Content-Length": []string{"3"}, } if !reflect.DeepEqual(got, want) { t.Errorf("Sanitized headers doesn't match expected value: wanted %+v, got %+v instead", want, got) } } httpretty-0.0.6/long_test.go 0000664 0000000 0000000 00000000362 13725530515 0016120 0 ustar 00root root 0000000 0000000 package httpretty import ( "io/ioutil" ) // from http://bastiat.org/fr/petition.html var petition = func() string { content, err := ioutil.ReadFile("testdata/petition.golden") if err != nil { panic(err) } return string(content) }() httpretty-0.0.6/norace_test.go 0000664 0000000 0000000 00000000065 13725530515 0016430 0 ustar 00root root 0000000 0000000 // +build !race package httpretty var Race = false httpretty-0.0.6/printer.go 0000664 0000000 0000000 00000041065 13725530515 0015612 0 ustar 00root root 0000000 0000000 package httpretty import ( "bufio" "bytes" "crypto/tls" "crypto/x509" "fmt" "io" "io/ioutil" "mime" "net" "net/http" "sort" "strings" "time" "github.com/henvic/httpretty/internal/color" "github.com/henvic/httpretty/internal/header" ) func newPrinter(l *Logger) printer { l.mu.Lock() defer l.mu.Unlock() return printer{ logger: l, flusher: l.flusher, } } type printer struct { flusher Flusher logger *Logger buf bytes.Buffer } func (p *printer) maybeOnReady() { if p.flusher == OnReady { p.flush() } } func (p *printer) flush() { if p.flusher == NoBuffer { return } p.logger.mu.Lock() defer p.logger.mu.Unlock() defer p.buf.Reset() w := p.logger.getWriter() fmt.Fprint(w, p.buf.String()) } func (p *printer) print(a ...interface{}) { p.logger.mu.Lock() defer p.logger.mu.Unlock() w := p.logger.getWriter() if p.flusher == NoBuffer { fmt.Fprint(w, a...) return } fmt.Fprint(&p.buf, a...) } func (p *printer) println(a ...interface{}) { p.logger.mu.Lock() defer p.logger.mu.Unlock() w := p.logger.getWriter() if p.flusher == NoBuffer { fmt.Fprintln(w, a...) return } fmt.Fprintln(&p.buf, a...) } func (p *printer) printf(format string, a ...interface{}) { p.logger.mu.Lock() defer p.logger.mu.Unlock() w := p.logger.getWriter() if p.flusher == NoBuffer { fmt.Fprintf(w, format, a...) return } fmt.Fprintf(&p.buf, format, a...) } func (p *printer) printRequest(req *http.Request) { if p.logger.RequestHeader { p.printRequestHeader(req) p.maybeOnReady() } if p.logger.RequestBody && req.Body != nil { p.printRequestBody(req) p.maybeOnReady() } } func (p *printer) printRequestInfo(req *http.Request) { to := req.URL.String() // req.URL.Host is empty on the request received by a server if req.URL.Host == "" { to = req.Host + to schema := "http://" if req.TLS != nil { schema = "https://" } to = schema + to } p.printf("* Request to %s\n", p.format(color.FgBlue, to)) if req.RemoteAddr != "" { p.printf("* Request from %s\n", p.format(color.FgBlue, req.RemoteAddr)) } } // checkFilter checkes if the request is filtered and if the Request value is nil. func (p *printer) checkFilter(req *http.Request) (skip bool) { filter := p.logger.getFilter() if req == nil { p.printf("> %s\n", p.format(color.FgRed, "error: null request")) return true } if filter == nil { return false } ok, err := safeFilter(filter, req) if err != nil { 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())) return false // never filter out the request if the filter errored } return ok } func safeFilter(filter Filter, req *http.Request) (skip bool, err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("panic: %v", e) } }() return filter(req) } func (p *printer) printResponse(resp *http.Response) { if resp == nil { p.printf("< %s\n", p.format(color.FgRed, "error: null response")) p.maybeOnReady() return } if p.logger.ResponseHeader { p.printResponseHeader(resp.Proto, resp.Status, resp.Header) p.maybeOnReady() } if p.logger.ResponseBody && resp.Body != nil && (resp.Request == nil || resp.Request.Method != http.MethodHead) { p.printResponseBodyOut(resp) p.maybeOnReady() } } func (p *printer) checkBodyFiltered(h http.Header) (skip bool, err error) { if f := p.logger.getBodyFilter(); f != nil { defer func() { if e := recover(); e != nil { p.printf("* panic while filtering body: %v\n", e) } }() return f(h) } return false, nil } func (p *printer) printResponseBodyOut(resp *http.Response) { if resp.ContentLength == 0 { return } skip, err := p.checkBodyFiltered(resp.Header) if err != nil { p.printf("* %s\n", p.format(color.FgRed, "error on response body filter: ", err.Error())) } if skip { return } if contentType := resp.Header.Get("Content-Type"); contentType != "" && isBinaryMediatype(contentType) { p.println("* body contains binary data") return } if p.logger.MaxResponseBody > 0 && resp.ContentLength > p.logger.MaxResponseBody { p.printf("* body is too long (%d bytes) to print, skipping (longer than %d bytes)\n", resp.ContentLength, p.logger.MaxResponseBody) return } contentType := resp.Header.Get("Content-Type") if resp.ContentLength == -1 { if newBody := p.printBodyUnknownLength(contentType, p.logger.MaxResponseBody, resp.Body); newBody != nil { resp.Body = newBody } return } var buf bytes.Buffer tee := io.TeeReader(resp.Body, &buf) defer resp.Body.Close() defer func() { resp.Body = ioutil.NopCloser(&buf) }() p.printBodyReader(contentType, tee) } // isBinary uses heuristics to guess if file is binary (actually, "printable" in the terminal). // See discussion at https://groups.google.com/forum/#!topic/golang-nuts/YeLL7L7SwWs func isBinary(body []byte) bool { if len(body) > 512 { body = body[512:] } // If file contains UTF-8 OR UTF-16 BOM, consider it non-binary. // Reference: https://tools.ietf.org/html/draft-ietf-websec-mime-sniff-03#section-5 if len(body) >= 3 && (bytes.Equal(body[:2], []byte{0xFE, 0xFF}) || // UTF-16BE BOM bytes.Equal(body[:2], []byte{0xFF, 0xFE}) || // UTF-16LE BOM bytes.Equal(body[:3], []byte{0xEF, 0xBB, 0xBF})) { // UTF-8 BOM return false } // If all of the first n octets are binary data octets, consider it binary. // Reference: https://github.com/golang/go/blob/349e7df2c3d0f9b5429e7c86121499c137faac7e/src/net/http/sniff.go#L297-L309 // c.f. section 5, step 4. for _, b := range body { switch { case b <= 0x08, b == 0x0B, 0x0E <= b && b <= 0x1A, 0x1C <= b && b <= 0x1F: return true } } // Otherwise, check against a white list of binary mimetypes. mediatype, _, err := mime.ParseMediaType(http.DetectContentType(body)) if err != nil { return false } return isBinaryMediatype(mediatype) } var binaryMediatypes = map[string]struct{}{ "application/pdf": struct{}{}, "application/postscript": struct{}{}, "image": struct{}{}, // for practical reasons, any image (including SVG) is considered binary data "audio": struct{}{}, "application/ogg": struct{}{}, "video": struct{}{}, "application/vnd.ms-fontobject": struct{}{}, "font": struct{}{}, "application/x-gzip": struct{}{}, "application/zip": struct{}{}, "application/x-rar-compressed": struct{}{}, "application/wasm": struct{}{}, } func isBinaryMediatype(mediatype string) bool { if _, ok := binaryMediatypes[mediatype]; ok { return true } if parts := strings.SplitN(mediatype, "/", 2); len(parts) == 2 { if _, ok := binaryMediatypes[parts[0]]; ok { return true } } return false } const maxDefaultUnknownReadable = 4096 // bytes func (p *printer) printBodyUnknownLength(contentType string, maxLength int64, r io.ReadCloser) (newBody io.ReadCloser) { shortReader := bufio.NewReader(r) if maxLength == 0 { maxLength = maxDefaultUnknownReadable } pb := make([]byte, maxLength+1) // read one extra bit to assure the length is longer than acceptable n, err := io.ReadFull(shortReader, pb) pb = pb[0:n] // trim any nil symbols left after writing in the byte slice. buf := bytes.NewReader(pb) newBody = newBodyReaderBuf(buf, r) switch { // Server requests always return req.Body != nil, but the Reader returns io.EOF immediately. // Avoiding returning early to mitigate any risk of bad reader implementations that might // send something even after returning io.EOF if read again. case err == io.EOF && n == 0: case err == nil && int64(n) > maxLength: p.printf("* body is too long, skipping (contains more than %d bytes)\n", n-1) case err == io.ErrUnexpectedEOF || err == nil: // cannot pass same bytes reader below because we only read it once. p.printBodyReader(contentType, bytes.NewReader(pb)) default: p.printf("* cannot read body: %v (%d bytes read)\n", err, n) } return } func findPeerCertificate(hostname string, state *tls.ConnectionState) (cert *x509.Certificate) { if chains := state.VerifiedChains; chains != nil && chains[0] != nil && chains[0][0] != nil { return chains[0][0] } if hostname == "" && len(state.PeerCertificates) > 0 { // skip finding a match for a given hostname if hostname is not available (e.g., a client certificate) return state.PeerCertificates[0] } // the chain is not created when tls.Config.InsecureSkipVerify is set, then let's try to find a match to display for _, cert := range state.PeerCertificates { if err := cert.VerifyHostname(hostname); err == nil { return cert } } return nil } func (p *printer) printTLSInfo(state *tls.ConnectionState, skipVerifyChains bool) { if state == nil { return } protocol := tlsProtocolVersions[state.Version] if protocol == "" { protocol = fmt.Sprintf("%#v", state.Version) } cipher := tlsCiphers[state.CipherSuite] if cipher == "" { cipher = fmt.Sprintf("%#v", state.CipherSuite) } p.printf("* TLS connection using %s / %s", p.format(color.FgBlue, protocol), p.format(color.FgBlue, cipher)) if !skipVerifyChains && state.VerifiedChains == nil { p.print(" (insecure=true)") } p.println() if state.NegotiatedProtocol != "" { p.printf("* ALPN: %v accepted\n", p.format(color.FgBlue, state.NegotiatedProtocol)) } } func (p *printer) printOutgoingClientTLS(config *tls.Config) { if config == nil || len(config.Certificates) == 0 { return } p.println("* Client certificate:") cert := config.Certificates[0].Leaf if cert == nil { // Please notice tls.Config.BuildNameToCertificate() doesn't store the certificate Leaf field. // You need to explicitly parse and store it with something such as: // cert.Leaf, err = x509.ParseCertificate(cert.Certificate) p.println(`** unparsed certificate found, skipping`) return } p.printCertificate("", cert) } func (p *printer) printIncomingClientTLS(state *tls.ConnectionState) { // if no TLS state is null or no client TLS certificate is found, return early. if state == nil || len(state.PeerCertificates) == 0 { return } p.println("* Client certificate:") cert := findPeerCertificate("", state) if cert == nil { p.println(p.format(color.FgRed, "** No valid certificate was found")) return } p.printCertificate("", cert) } func (p *printer) printTLSServer(host string, state *tls.ConnectionState) { if state == nil { return } hostname, _, err := net.SplitHostPort(host) if err != nil { // assume the error is due to "missing port in address" hostname = host } p.println("* Server certificate:") cert := findPeerCertificate(hostname, state) if cert == nil { p.println(p.format(color.FgRed, "** No valid certificate was found")) return } // server certificate messages are slightly similar to how "curl -v" shows p.printCertificate(hostname, cert) } func (p *printer) printCertificate(hostname string, cert *x509.Certificate) { p.printf(`* subject: %v * start date: %v * expire date: %v * issuer: %v `, p.format(color.FgBlue, cert.Subject), p.format(color.FgBlue, cert.NotBefore.Format(time.UnixDate)), p.format(color.FgBlue, cert.NotAfter.Format(time.UnixDate)), p.format(color.FgBlue, cert.Issuer), ) if hostname == "" { return } if err := cert.VerifyHostname(hostname); err != nil { p.printf("* %s\n", p.format(color.FgRed, err.Error())) return } p.println("* TLS certificate verify ok.") } func (p *printer) printServerResponse(req *http.Request, rec *responseRecorder) { if p.logger.ResponseHeader { // TODO(henvic): see how httptest.ResponseRecorder adds extra headers due to Content-Type detection // and other stuff (Date). It would be interesting to show them here too (either as default or opt-in). p.printResponseHeader(req.Proto, fmt.Sprintf("%d %s", rec.statusCode, http.StatusText(rec.statusCode)), rec.Header()) } if !p.logger.ResponseBody || rec.size == 0 { return } skip, err := p.checkBodyFiltered(rec.Header()) if err != nil { p.printf("* %s\n", p.format(color.FgRed, "error on response body filter: ", err.Error())) } if skip { return } if mediatype := req.Header.Get("Content-Type"); mediatype != "" && isBinaryMediatype(mediatype) { p.println("* body contains binary data") return } if p.logger.MaxResponseBody > 0 && rec.size > p.logger.MaxResponseBody { p.printf("* body is too long (%d bytes) to print, skipping (longer than %d bytes)\n", rec.size, p.logger.MaxResponseBody) return } p.printBodyReader(rec.Header().Get("Content-Type"), rec.buf) } func (p *printer) printResponseHeader(proto, status string, h http.Header) { p.printf("< %s %s\n", p.format(color.FgBlue, color.Bold, proto), p.format(color.FgRed, status)) p.printHeaders('<', h) p.println() } func (p *printer) printBodyReader(contentType string, r io.Reader) { mediatype, _, _ := mime.ParseMediaType(contentType) body, err := ioutil.ReadAll(r) if err != nil { p.printf("* cannot read body: %v\n", p.format(color.FgRed, err.Error())) return } if isBinary(body) { p.println("* body contains binary data") return } for _, f := range p.logger.Formatters { if ok := p.safeBodyMatch(f, mediatype); !ok { continue } var formatted bytes.Buffer switch err := p.safeBodyFormat(f, &formatted, body); { case err != nil: p.printf("* body cannot be formatted: %v\n%s\n", p.format(color.FgRed, err.Error()), string(body)) default: p.println(formatted.String()) } return } p.println(string(body)) } func (p *printer) safeBodyMatch(f Formatter, mediatype string) bool { defer func() { if e := recover(); e != nil { p.printf("* panic while testing body format: %v\n", e) } }() return f.Match(mediatype) } func (p *printer) safeBodyFormat(f Formatter, w io.Writer, src []byte) (err error) { defer func() { // should not return panic as error because we want to try the next formatter if e := recover(); e != nil { err = fmt.Errorf("panic: %v", e) } }() return f.Format(w, src) } func (p *printer) format(s ...interface{}) string { if p.logger.Colors { return color.Format(s...) } return color.StripAttributes(s...) } func (p *printer) printHeaders(prefix rune, h http.Header) { if !p.logger.SkipSanitize { h = header.Sanitize(header.DefaultSanitizers, h) } skipped := p.logger.cloneSkipHeader() for _, key := range sortHeaderKeys(h) { for _, v := range h[key] { if _, skip := skipped[key]; skip { continue } p.printf("%c %s%s %s\n", prefix, p.format(color.FgBlue, color.Bold, key), p.format(color.FgRed, ":"), p.format(color.FgYellow, v)) } } } func sortHeaderKeys(h http.Header) []string { keys := make([]string, 0, len(h)) for key := range h { keys = append(keys, key) } sort.Strings(keys) return keys } func (p *printer) printRequestHeader(req *http.Request) { p.printf("> %s %s %s\n", p.format(color.FgBlue, color.Bold, req.Method), p.format(color.FgYellow, req.URL.RequestURI()), p.format(color.FgBlue, req.Proto)) host := req.Host if host == "" { host = req.URL.Host } if host != "" { p.printf("> %s%s %s\n", p.format(color.FgBlue, color.Bold, "Host"), p.format(color.FgRed, ":"), p.format(color.FgYellow, host), ) } p.printHeaders('>', req.Header) p.println() } func (p *printer) printRequestBody(req *http.Request) { // For client requests, a request with zero content-length and no body is also treated as unknown. if req.Body == nil { return } skip, err := p.checkBodyFiltered(req.Header) if err != nil { p.printf("* %s\n", p.format(color.FgRed, "error on request body filter: ", err.Error())) } if skip { return } if mediatype := req.Header.Get("Content-Type"); mediatype != "" && isBinaryMediatype(mediatype) { p.println("* body contains binary data") return } // TODO(henvic): add support for printing multipart/formdata information as body (to responses too). if p.logger.MaxRequestBody > 0 && req.ContentLength > p.logger.MaxRequestBody { p.printf("* body is too long (%d bytes) to print, skipping (longer than %d bytes)\n", req.ContentLength, p.logger.MaxRequestBody) return } contentType := req.Header.Get("Content-Type") if req.ContentLength > 0 { var buf bytes.Buffer tee := io.TeeReader(req.Body, &buf) defer req.Body.Close() defer func() { req.Body = ioutil.NopCloser(&buf) }() p.printBodyReader(contentType, tee) return } if newBody := p.printBodyUnknownLength(contentType, p.logger.MaxRequestBody, req.Body); newBody != nil { req.Body = newBody } } func (p *printer) printTimeRequest() (end func()) { startRequest := time.Now() p.printf("* Request at %v\n", startRequest) return func() { p.printf("* Request took %v\n", time.Since(startRequest)) } } httpretty-0.0.6/race_test.go 0000664 0000000 0000000 00000000471 13725530515 0016074 0 ustar 00root root 0000000 0000000 // +build race package httpretty // Race is a flag that can be usde to detect whether the race detector is on. // It was added because as of Go 1.13.7 the TestOutgoingConcurrency test is failing because of a bug on the // net/http standard library package. // See https://golang.org/issue/30597 var Race = true httpretty-0.0.6/recorder.go 0000664 0000000 0000000 00000002161 13725530515 0015726 0 ustar 00root root 0000000 0000000 package httpretty import ( "bytes" "io" "net/http" ) type bodyCloser struct { r io.Reader close func() error } func (bc *bodyCloser) Read(p []byte) (n int, err error) { return bc.r.Read(p) } func (bc *bodyCloser) Close() error { return bc.close() } func newBodyReaderBuf(buf io.Reader, body io.ReadCloser) *bodyCloser { return &bodyCloser{ r: io.MultiReader(buf, body), close: body.Close, } } type responseRecorder struct { http.ResponseWriter statusCode int maxReadableBody int64 size int64 buf *bytes.Buffer } // Write the data to the connection as part of an HTTP reply, and records it. func (rr *responseRecorder) Write(p []byte) (int, error) { rr.size += int64(len(p)) if rr.maxReadableBody > 0 && rr.size > rr.maxReadableBody { rr.buf = nil return rr.ResponseWriter.Write(p) } defer rr.buf.Write(p) return rr.ResponseWriter.Write(p) } // WriteHeader sends an HTTP response header with the provided // status code, and records it. func (rr *responseRecorder) WriteHeader(statusCode int) { rr.ResponseWriter.WriteHeader(statusCode) rr.statusCode = statusCode } httpretty-0.0.6/scripts/ 0000775 0000000 0000000 00000000000 13725530515 0015261 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/scripts/coverage.sh 0000775 0000000 0000000 00000002606 13725530515 0017417 0 ustar 00root root 0000000 0000000 #!/bin/bash # Modified version of chef-runner/script/coverage # Copyright 2004 Mathias Lafeldt # Apache License 2.0 # Source: https://github.com/mlafeldt/chef-runner/blob/v0.7.0/script/coverage # Generate test coverage statistics for Go packages. # # Works around the fact that `go test -coverprofile` currently does not work # with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909 # # Usage: script/coverage [--html|--coveralls] # # --html Additionally create HTML report and open it in browser # --coveralls Push coverage statistics to coveralls.io # # Changes: directories ending in .go used to fail set -e workdir=.cover profile="$workdir/cover.out" mode=count generate_cover_data() { rm -rf "$workdir" mkdir "$workdir" for pkg in "$@"; do f="$workdir/$(echo $pkg | tr / -).cover" go test -covermode="$mode" -coverprofile="$f" "$pkg/" done echo "mode: $mode" >"$profile" grep -h -v "^mode:" "$workdir"/*.cover >>"$profile" } show_cover_report() { go tool cover -${1}="$profile" } push_to_coveralls() { echo "Pushing coverage statistics to coveralls.io" goveralls -coverprofile="$profile" } generate_cover_data $(go list ./...) show_cover_report func case "$1" in "") ;; --html) show_cover_report html ;; --coveralls) push_to_coveralls ;; *) echo >&2 "error: invalid option: $1"; exit 1 ;; esac httpretty-0.0.6/scripts/install-ci.sh 0000775 0000000 0000000 00000000616 13725530515 0017662 0 ustar 00root root 0000000 0000000 #!/bin/bash set -euox pipefail # TODO(henvic): install specific versions of the commands # when the 3 latest releases of the Go toolchains supports it using @tag. go install github.com/mattn/goveralls go install golang.org/x/lint/golint go install honnef.co/go/tools/cmd/staticcheck go install github.com/securego/gosec/cmd/gosec go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow httpretty-0.0.6/scripts/test.sh 0000775 0000000 0000000 00000000717 13725530515 0016604 0 ustar 00root root 0000000 0000000 #!/bin/bash set -euo pipefail IFS=$'\n\t' # Static analysis scripts cd $(dirname $0)/.. echo "Linting code." test -z "$(golint `go list ./...` | tee /dev/stderr)" echo "Examining source code against code defect." go vet $(go list ./...) go vet -vettool=$(which shadow) echo "Running staticcheck toolset." staticcheck ./... echo "Checking if code contains security issues." gosec -quiet ./... echo "Running tests with data race detector" go test ./... -race httpretty-0.0.6/server_test.go 0000664 0000000 0000000 00000120213 13725530515 0016465 0 ustar 00root root 0000000 0000000 package httpretty import ( "bytes" "context" "crypto/tls" "crypto/x509" "errors" "fmt" "io/ioutil" "mime" "mime/multipart" "net" "net/http" "net/http/httptest" "net/url" "strings" "sync" "testing" "time" ) // inspect a request (not concurrency safe). func inspect(next http.Handler, wait int) *inspectHandler { is := &inspectHandler{ next: next, } is.wg.Add(wait) return is } type inspectHandler struct { next http.Handler wg sync.WaitGroup req *http.Request } func (h *inspectHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { h.req = req h.next.ServeHTTP(w, req) h.wg.Done() } func (h *inspectHandler) Wait() { h.wg.Wait() } func TestIncoming(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } go func() { client := newServerClient() resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) }() is.Wait() want := fmt.Sprintf(`* Request to http://%s/ * Request from %s > GET / HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK Hello, world! `, is.req.Host, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingNotFound(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, ResponseHeader: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(http.NotFoundHandler()), 1) ts := httptest.NewServer(is) defer ts.Close() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } go func() { client := newServerClient() resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } if resp.StatusCode != http.StatusNotFound { t.Errorf("got status codem %v, wanted %v", resp.StatusCode, http.StatusNotFound) } }() is.Wait() want := fmt.Sprintf(`* Request to http://%s/ * Request from %s > GET / HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 404 Not Found < Content-Type: text/plain; charset=utf-8 < X-Content-Type-Options: nosniff `, is.req.Host, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func outgoingGetServer(client *http.Client, ts *httptest.Server, done func()) { defer done() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { panic(err) } if _, err := client.Do(req); err != nil { panic(err) } } func TestIncomingConcurrency(t *testing.T) { logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } logger.SetFlusher(OnEnd) var buf bytes.Buffer logger.SetOutput(&buf) ts := httptest.NewServer(logger.Middleware(helloHandler{})) defer ts.Close() concurrency := 100 { var wg sync.WaitGroup wg.Add(concurrency) i := 0 repeat: client := &http.Client{ Transport: newTransport(), } go outgoingGetServer(client, ts, wg.Done) if i < concurrency-1 { i++ time.Sleep(2 * time.Millisecond) goto repeat } wg.Wait() } got := buf.String() gotConcurrency := strings.Count(got, "< HTTP/1.1 200 OK") if concurrency != gotConcurrency { t.Errorf("logged %d requests, wanted %d", concurrency, gotConcurrency) } want := fmt.Sprintf(`> GET / HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK Hello, world!`, ts.Listener.Addr()) if !strings.Contains(got, want) { t.Errorf("Request doesn't contain expected body") } } func TestIncomingMinimal(t *testing.T) { t.Parallel() // only prints the request URI and remote address that requested it. logger := &Logger{} var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/incoming", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") req.AddCookie(&http.Cookie{ Name: "food", Value: "sorbet", }) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s `, uri, is.req.RemoteAddr) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingSanitized(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/incoming", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") req.AddCookie(&http.Cookie{ Name: "food", Value: "sorbet", }) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /incoming HTTP/1.1 > Host: %s > Accept-Encoding: gzip > Cookie: food=████████████████████ > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK Hello, world! `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } type hideHandler struct { next http.Handler } func (h hideHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { req = req.WithContext(WithHide(context.Background())) h.next.ServeHTTP(w, req) } func TestIncomingHide(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(hideHandler{ next: logger.Middleware(helloHandler{}), }, 1) ts := httptest.NewServer(is) defer ts.Close() go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() if buf.Len() != 0 { t.Errorf("request should not be logged, got %v", buf.String()) } } func TestIncomingFilter(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SetFilter(filteredURIs) ts := httptest.NewServer(logger.Middleware(helloHandler{})) defer ts.Close() testCases := []struct { uri string want string }{ {uri: "filtered"}, {uri: "unfiltered", want: "* Request"}, {uri: "other", want: "filter error triggered"}, } for _, tc := range testCases { t.Run(tc.uri, func(t *testing.T) { var buf bytes.Buffer logger.SetOutput(&buf) client := newServerClient() _, err := client.Get(fmt.Sprintf("%s/%s", ts.URL, tc.uri)) if err != nil { t.Errorf("cannot create request: %v", err) } if tc.want == "" && buf.Len() != 0 { t.Errorf("wanted input to be filtered, got %v instead", buf.String()) } if !strings.Contains(buf.String(), tc.want) { t.Errorf(`expected input to contain "%v", got %v instead`, tc.want, buf.String()) } }) } } func TestIncomingFilterPanicked(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SetFilter(func(req *http.Request) (bool, error) { panic("evil panic") }) is := inspect(logger.Middleware(helloHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() _, err := client.Get(ts.URL) if err != nil { t.Errorf("cannot create request: %v", err) } want := fmt.Sprintf(`* cannot filter request: GET /: panic: evil panic * Request to %v/ * Request from %v > GET / HTTP/1.1 > Host: %v > Accept-Encoding: gzip > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK Hello, world! `, ts.URL, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf(`expected input to contain "%v", got %v instead`, want, got) } } func TestIncomingSkipHeader(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SkipHeader([]string{ "user-agent", "content-type", }) is := inspect(logger.Middleware(jsonHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() uri := fmt.Sprintf("%s/json", ts.URL) go func() { req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip < HTTP/1.1 200 OK {"result":"Hello, world!","number":3.14} `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingBodyFilter(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { mediatype, _, _ := mime.ParseMediaType(h.Get("Content-Type")) return mediatype == "application/json", nil }) is := inspect(logger.Middleware(jsonHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() uri := fmt.Sprintf("%s/json", ts.URL) go func() { req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingBodyFilterSoftError(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { // filter anyway, but print soft error saying something went wrong during the filtering. return true, errors.New("incomplete implementation") }) is := inspect(logger.Middleware(jsonHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() uri := fmt.Sprintf("%s/json", ts.URL) go func() { req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com * error on request body filter: incomplete implementation < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 * error on response body filter: incomplete implementation `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingBodyFilterPanicked(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.SetBodyFilter(func(h http.Header) (skip bool, err error) { panic("evil panic") }) is := inspect(logger.Middleware(jsonHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() uri := fmt.Sprintf("%s/json", ts.URL) go func() { req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com * panic while filtering body: evil panic < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 * panic while filtering body: evil panic {"result":"Hello, world!","number":3.14} `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingWithTimeRequest(t *testing.T) { t.Parallel() logger := &Logger{ Time: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() go func() { client := &http.Client{ Transport: newTransport(), } req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() got := buf.String() if !strings.Contains(got, "* Request at ") { t.Error("missing printing start time of request") } if !strings.Contains(got, "* Request took ") { t.Error("missing printing request duration") } } func TestIncomingFormattedJSON(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } is := inspect(logger.Middleware(jsonHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() uri := fmt.Sprintf("%s/json", ts.URL) go func() { req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 { "result": "Hello, world!", "number": 3.14 } `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingBadJSON(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } is := inspect(logger.Middleware(badJSONHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/json", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 * body cannot be formatted: invalid character '}' looking for beginning of value {"bad": } `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingFormatterPanicked(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &panickingFormatter{}, } is := inspect(logger.Middleware(badJSONHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/json", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 * body cannot be formatted: panic: evil formatter {"bad": } `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingFormatterMatcherPanicked(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &panickingFormatterMatcher{}, } is := inspect(logger.Middleware(badJSONHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/json", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /json HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 * panic while testing body format: evil matcher {"bad": } `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingForm(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(formHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() logger.Formatters = []Formatter{ &JSONFormatter{}, } uri := fmt.Sprintf("%s/form", ts.URL) go func() { client := newServerClient() form := url.Values{} form.Add("foo", "bar") form.Add("email", "root@example.com") req, err := http.NewRequest(http.MethodPost, uri, strings.NewReader(form.Encode())) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > POST /form HTTP/1.1 > Host: %s > Accept-Encoding: gzip > Content-Length: 32 > User-Agent: Go-http-client/1.1 email=root%%40example.com&foo=bar < HTTP/1.1 200 OK form received `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingBinaryBody(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") })), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/convert", ts.URL) go func() { client := newServerClient() b := []byte("RIFF\x00\x00\x00\x00WEBPVP") req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) req.Header.Add("Content-Type", "image/webp") if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > POST /convert HTTP/1.1 > Host: %s > Accept-Encoding: gzip > Content-Length: 14 > Content-Type: image/webp > User-Agent: Go-http-client/1.1 * body contains binary data < HTTP/1.1 200 OK * body contains binary data `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingBinaryBodyNoMediatypeHeader(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header()["Date"] = nil w.Header()["Content-Type"] = nil fmt.Fprint(w, "\x25\x50\x44\x46\x2d\x31\x2e\x33\x0a\x25\xc4\xe5\xf2\xe5\xeb\xa7") })), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/convert", ts.URL) go func() { client := newServerClient() b := []byte("RIFF\x00\x00\x00\x00WEBPVP") req, err := http.NewRequest(http.MethodPost, uri, bytes.NewReader(b)) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > POST /convert HTTP/1.1 > Host: %s > Accept-Encoding: gzip > Content-Length: 14 > User-Agent: Go-http-client/1.1 * body contains binary data < HTTP/1.1 200 OK * body contains binary data `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingLongRequest(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(longRequestHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/long-request", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodPut, uri, strings.NewReader(petition)) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > PUT /long-request HTTP/1.1 > Host: %s > Accept-Encoding: gzip > Content-Length: 9846 > User-Agent: Go-http-client/1.1 %s < HTTP/1.1 200 OK long request received `, uri, is.req.RemoteAddr, ts.Listener.Addr(), petition) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingLongResponse(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = int64(len(petition) + 1000) // value larger than the text is := inspect(logger.Middleware(longResponseHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/long-response", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte(petition)) }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /long-response HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK < Content-Length: 9846 %s `, uri, is.req.RemoteAddr, ts.Listener.Addr(), petition) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingLongResponseHead(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = int64(len(petition) + 1000) // value larger than the text is := inspect(logger.Middleware(longResponseHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() client := newServerClient() uri := fmt.Sprintf("%s/long-response", ts.URL) go func() { req, err := http.NewRequest(http.MethodHead, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > HEAD /long-response HTTP/1.1 > Host: %s > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK < Content-Length: 9846 `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingTooLongResponse(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = 5000 // value smaller than the text is := inspect(logger.Middleware(longResponseHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/long-response", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte(petition)) }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /long-response HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK < Content-Length: 9846 * body is too long (9846 bytes) to print, skipping (longer than 5000 bytes) `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingLongResponseUnknownLength(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, MaxResponseBody: 10000000, } var buf bytes.Buffer logger.SetOutput(&buf) repeat := 100 is := inspect(logger.Middleware(longResponseUnknownLengthHandler{repeat: repeat}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/long-response", ts.URL) repeatedBody := strings.Repeat(petition, repeat+1) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte(repeatedBody)) }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /long-response HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK %s `, uri, is.req.RemoteAddr, ts.Listener.Addr(), repeatedBody) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingLongResponseUnknownLengthTooLong(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.MaxResponseBody = 5000 // value smaller than the text is := inspect(logger.Middleware(longResponseUnknownLengthHandler{}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/long-response", ts.URL) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodGet, uri, nil) if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte(petition)) }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /long-response HTTP/1.1 > Host: %s > Accept-Encoding: gzip > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK * body is too long (9846 bytes) to print, skipping (longer than 5000 bytes) `, uri, is.req.RemoteAddr, ts.Listener.Addr()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingMultipartForm(t *testing.T) { t.Parallel() logger := &Logger{ RequestHeader: true, // TODO(henvic): print request body once support for printing out multipart/formdata body is added. ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) logger.Formatters = []Formatter{ &JSONFormatter{}, } is := inspect(logger.Middleware(multipartHandler{t}), 1) ts := httptest.NewServer(is) defer ts.Close() uri := fmt.Sprintf("%s/multipart-upload", ts.URL) body := &bytes.Buffer{} writer := multipart.NewWriter(body) multipartTestdata(writer, body) go func() { client := newServerClient() req, err := http.NewRequest(http.MethodPost, uri, body) if err != nil { t.Errorf("cannot create request: %v", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) _, err = client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > POST /multipart-upload HTTP/1.1 > Host: %s > Accept-Encoding: gzip > Content-Length: 10355 > Content-Type: %s > User-Agent: Go-http-client/1.1 < HTTP/1.1 200 OK upload received `, uri, is.req.RemoteAddr, ts.Listener.Addr(), writer.FormDataContentType()) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingTLS(t *testing.T) { t.Parallel() logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) ts := httptest.NewTLSServer(is) defer ts.Close() go func() { client := ts.Client() req, err := http.NewRequest(http.MethodGet, ts.URL, nil) req.Host = "example.com" // overriding the Host header to send req.Header.Add("User-Agent", "Robot/0.1 crawler@example.com") if err != nil { t.Errorf("cannot create request: %v", err) } resp, err := client.Do(req) if err != nil { t.Errorf("cannot connect to the server: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) }() is.Wait() want := fmt.Sprintf(`* Request to https://example.com/ * Request from %s * TLS connection using TLS 1.3 / TLS_AES_128_GCM_SHA256 > GET / HTTP/1.1 > Host: example.com > Accept-Encoding: gzip > User-Agent: Robot/0.1 crawler@example.com < HTTP/1.1 200 OK Hello, world! `, is.req.RemoteAddr) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingMutualTLS(t *testing.T) { t.Parallel() caCert, err := ioutil.ReadFile("testdata/cert.pem") if err != nil { panic(err) } clientCert, err := ioutil.ReadFile("testdata/cert-client.pem") if err != nil { panic(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(clientCert) tlsConfig := &tls.Config{ ClientCAs: caCertPool, ClientAuth: tls.RequireAndVerifyClientCert, } logger := &Logger{ TLS: true, RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) // NOTE(henvic): Using httptest directly turned out complicated. // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go server := &http.Server{ TLSConfig: tlsConfig, Handler: is, } listener, err := netListener() if err != nil { panic(fmt.Sprintf("failed to listen on a port: %v", err)) } defer listener.Close() go func() { // Certificate generated with // $ openssl req -x509 -newkey rsa:2048 \ // -new -nodes -sha256 \ // -days 36500 \ // -out cert.pem \ // -keyout key.pem \ // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { t.Errorf("server exit with unexpected error: %v", errcp) } }() defer server.Shutdown(context.Background()) // Certificate generated with // $ openssl req -newkey rsa:2048 \ // -new -nodes -x509 \ // -days 36500 \ // -out cert-client.pem \ // -keyout key-client.pem \ // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") if err != nil { t.Errorf("failed to load X509 key pair: %v", err) } cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) if err != nil { t.Errorf("failed to parse certificate for copying Leaf field") } // Create a HTTPS client and supply the created CA pool and certificate clientTLSConfig := &tls.Config{ RootCAs: caCertPool, Certificates: []tls.Certificate{cert}, } _, port, err := net.SplitHostPort(listener.Addr().String()) if err != nil { panic(err) } host := fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) go func() { transport := newTransport() transport.TLSClientConfig = clientTLSConfig client := &http.Client{ Transport: transport, } resp, err := client.Get(host) if err != nil { t.Errorf("cannot create request: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s * TLS connection using TLS 1.3 / TLS_AES_128_GCM_SHA256 * ALPN: h2 accepted * Client certificate: * subject: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL * start date: Sat Jan 25 20:12:36 UTC 2020 * expire date: Mon Jan 1 20:12:36 UTC 2120 * issuer: CN=User,OU=User,O=Client,L=Rotterdam,ST=Zuid-Holland,C=NL > GET /mutual-tls-test HTTP/2.0 > Host: localhost:%s > Accept-Encoding: gzip > User-Agent: Go-http-client/2.0 < HTTP/2.0 200 OK Hello, world! `, host, is.req.RemoteAddr, port) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func TestIncomingMutualTLSNoSafetyLogging(t *testing.T) { t.Parallel() caCert, err := ioutil.ReadFile("testdata/cert.pem") if err != nil { panic(err) } clientCert, err := ioutil.ReadFile("testdata/cert-client.pem") if err != nil { panic(err) } caCertPool := x509.NewCertPool() caCertPool.AppendCertsFromPEM(caCert) caCertPool.AppendCertsFromPEM(clientCert) tlsConfig := &tls.Config{ ClientCAs: caCertPool, ClientAuth: tls.RequireAndVerifyClientCert, } logger := &Logger{ // TLS must be false RequestHeader: true, RequestBody: true, ResponseHeader: true, ResponseBody: true, } var buf bytes.Buffer logger.SetOutput(&buf) is := inspect(logger.Middleware(helloHandler{}), 1) // NOTE(henvic): Using httptest directly turned out complicated. // See https://venilnoronha.io/a-step-by-step-guide-to-mtls-in-go server := &http.Server{ TLSConfig: tlsConfig, Handler: is, } listener, err := netListener() if err != nil { panic(fmt.Sprintf("failed to listen on a port: %v", err)) } defer listener.Close() go func() { // Certificate generated with // $ openssl req -x509 -newkey rsa:2048 \ // -new -nodes -sha256 \ // -days 36500 \ // -out cert.pem \ // -keyout key.pem \ // -subj "/C=US/ST=California/L=Carmel-by-the-Sea/O=Plifk/OU=Cloud/CN=localhost" -extensions EXT -config <( \ // printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth, clientAuth") if errcp := server.ServeTLS(listener, "testdata/cert.pem", "testdata/key.pem"); errcp != http.ErrServerClosed { t.Errorf("server exit with unexpected error: %v", errcp) } }() defer server.Shutdown(context.Background()) // Certificate generated with // $ openssl req -newkey rsa:2048 \ // -new -nodes -x509 \ // -days 36500 \ // -out cert-client.pem \ // -keyout key-client.pem \ // -subj "/C=NL/ST=Zuid-Holland/L=Rotterdam/O=Client/OU=User/CN=User" cert, err := tls.LoadX509KeyPair("testdata/cert-client.pem", "testdata/key-client.pem") if err != nil { t.Errorf("failed to load X509 key pair: %v", err) } cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) if err != nil { t.Errorf("failed to parse certificate for copying Leaf field") } // Create a HTTPS client and supply the created CA pool and certificate clientTLSConfig := &tls.Config{ RootCAs: caCertPool, Certificates: []tls.Certificate{cert}, } _, port, err := net.SplitHostPort(listener.Addr().String()) if err != nil { panic(err) } host := fmt.Sprintf("https://localhost:%s/mutual-tls-test", port) go func() { transport := newTransport() transport.TLSClientConfig = clientTLSConfig client := &http.Client{ Transport: transport, } resp, err := client.Get(host) if err != nil { t.Errorf("cannot create request: %v", err) } testBody(t, resp.Body, []byte("Hello, world!")) }() is.Wait() want := fmt.Sprintf(`* Request to %s * Request from %s > GET /mutual-tls-test HTTP/2.0 > Host: localhost:%s > Accept-Encoding: gzip > User-Agent: Go-http-client/2.0 < HTTP/2.0 200 OK Hello, world! `, host, is.req.RemoteAddr, port) if got := buf.String(); got != want { t.Errorf("logged HTTP request %s; want %s", got, want) } } func newServerClient() *http.Client { return &http.Client{ Transport: newTransport(), } } httpretty-0.0.6/testdata/ 0000775 0000000 0000000 00000000000 13725530515 0015403 5 ustar 00root root 0000000 0000000 httpretty-0.0.6/testdata/cert-client.pem 0000664 0000000 0000000 00000002264 13725530515 0020323 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE----- MIIDTDCCAjQCCQC9tIz6aPdvETANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJO TDEVMBMGA1UECAwMWnVpZC1Ib2xsYW5kMRIwEAYDVQQHDAlSb3R0ZXJkYW0xDzAN BgNVBAoMBkNsaWVudDENMAsGA1UECwwEVXNlcjENMAsGA1UEAwwEVXNlcjAgFw0y MDAxMjUyMDEyMzZaGA8yMTIwMDEwMTIwMTIzNlowZzELMAkGA1UEBhMCTkwxFTAT BgNVBAgMDFp1aWQtSG9sbGFuZDESMBAGA1UEBwwJUm90dGVyZGFtMQ8wDQYDVQQK DAZDbGllbnQxDTALBgNVBAsMBFVzZXIxDTALBgNVBAMMBFVzZXIwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54gG9rMrVioL9i3sHSkhE1iihNKAjRd+W S6iG60wNl7PeQcZvQhgf1d9/tZDyIM5mP0XaCRXxfRAHJeXDqjeLkGyqPPRNFgvU kzJDnKisZ7cPANqfWHJZz/qQF+ePvAnZiBEhp+9BXUfjiAHIMglvwn3W3s54d+0V DzmgZp1ha1LG/iU2MkHDpcNM/C8edtd9UmGG0mIHS0H1JpNfuRNjPeFdCVvsi0wO ZpIDWqzKiyCbro0IakxXStMIFwwpFEGgxkOytpJeqjdnEklTXxb0gxCthB9WKszz RsQfZ5UVW9guhYfp/a49cbcMO//qM3E6V/Pff9bLjkNLyC0ggi2BAgMBAAEwDQYJ KoZIhvcNAQELBQADggEBAC4RtmjCTFKKHJu51ic7vtIiH4Xc2nidAajrxPdb5a7C 9jYPmCcH4atbv3ce4VFJ0Fcq3M3MS2mIei9Y+vn1GfkLOe8zdT1hWmDLgZttj06x L5Zxm0hTcMz9miJp6HQ+JRZNSqgk0OnGLaT3W5fYEI68Aei1udAI2pGPRD1OwRwJ r/qSHUF8K0He2pbaL/czkfI5hADicawGNggalSF8rxmmzSc+qXvL1vk0BjqqUdES SvGZNabw7PfFAHWwl1BU3/xJRTL9X/xoZnQjlf+ljdab7MP3Je5He2wj9PCgJzA9 HYl9VyKwmqLR/7Z941A+k9Xtf4/ykxL7Tr7qZAtRrIE= -----END CERTIFICATE----- httpretty-0.0.6/testdata/cert.pem 0000664 0000000 0000000 00000002466 13725530515 0017053 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE----- MIIDrTCCApWgAwIBAgIJAJH7PYwIA1bFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRowGAYDVQQHDBFDYXJtZWwtYnkt dGhlLVNlYTEOMAwGA1UECgwFUGxpZmsxDjAMBgNVBAsMBUNsb3VkMRIwEAYDVQQD DAlsb2NhbGhvc3QwIBcNMjAwODEyMjIyMDQ1WhgPMjEyMDA3MTkyMjIwNDVaMHIx CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRowGAYDVQQHDBFDYXJt ZWwtYnktdGhlLVNlYTEOMAwGA1UECgwFUGxpZmsxDjAMBgNVBAsMBUNsb3VkMRIw EAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQCvoOFz4m47kl5mw8In6LFC1Tanmo1vG4SscoU9XCg4Pht33qXE3CGOTQuNKtNm VVO4P1eCCjd8fwaRr0V04Wg/RK+AhneGxhMuQlebJxEEE4e4AoFyJzYeqcm6HLdK D2SLP9icajOUnF5ZerYns32sU34/htSqA8jdDBNoND0kPwCckvYGQu875n2V1BdJ LEYAyv1oOPG9Ec45nkyBApv9102WxICMf35O5XOKcegkp1g75D/ModNGNJ49k7ZL AajbRq5jkToo+u8LDkGmdavPO625bE7Roo24fAjUTVD/mYKAKzbqDtsrDOfyq8LM cH0Br/29vjCb/jKoe4e5A+j3AgMBAAGjRDBCMBQGA1UdEQQNMAuCCWxvY2FsaG9z dDALBgNVHQ8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0G CSqGSIb3DQEBCwUAA4IBAQAKdhOh1nxe0xZD3czfDyrCB1bals6rnREg4xvKnpc6 99F61S8+4SSbJIIWkP5BjNiP36czxi0h0yyXjO+WP9gnGHdcZmEwtXVHTMoC7Ql/ gmJ+xvls7NF08lOCnNjzh5Vb+2bEtckFSV2v0m1BsngSfcVLmZHxw44Hxa0nBAbi 1tYunESZxaJrB4snJHrvYfctTHa08XWVoAXNkZj/4fpHAoulkXfK+zczU/UVnWnW WJGHd8Beo6OE6AQ4RNewKW8K+jBv4CAu+wA5713O3Ys/GMxDZdS02Em4/IjFObFE YrHRtvpSrTxACFsw8XQbFy525XBoEWepqiZHRry+6ddl -----END CERTIFICATE----- httpretty-0.0.6/testdata/cert_example.pem 0000664 0000000 0000000 00000002117 13725530515 0020557 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE----- MIIDADCCAeigAwIBAgIQOLOnQdiwgDrspBPLtc9dBzANBgkqhkiG9w0BAQsFADAS MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAqkMAE2yt/xG/N9W4hVayxCWjr0idQQy6Roirdp8lk7rwH6heQZFB EyZuysonJAA/aEAt3l9914UMwlA80o9UeTc+CNqnP6oK02XBq8kbLwYQYSp1rUnD G2GioerFVE9si/lIRkt5c8XY5ocEKCdnh+ETzu0o18ClAFYwEywJaAuUQ9xFpe+j oaqOiy1DjAc/14MNuoBhUpiDLhwTrarMX0npzELp5nd8jmwYsN3YRsHMJ7HbfcN/ WNP475E28o+5MM7xh9hDuOKeRBlRz0jToR/pFVK+1aFaRToz7An2H/NUYgVLHqMw Hfd+T31zXOy+nfEvXYWnqiaL6+79W2krEQIDAQABo1AwTjAOBgNVHQ8BAf8EBAMC AqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB/zAWBgNVHREE DzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAWhbA93uT9XyqaeSQ M2OsZY+4Y8wuyn0A7u6/YPhhb3HcWYIZ7BXjp/1f8JhZV32eLQ+qdK3ERAnHT0uR xVPsZKL1hwZkp4uIBAZ2x883EcZ+nZkN46BpKAfcOWdQLFuX3LUXWCgbkPf1Y/sc Oec7RCBF2qDlVKTHXmaoTQq4WXtKBrE5ekwxmU+/qrdwXkzQ9HNQWAlRCdHEtoEh cXwjtLBIvlessb7pptqaB7qTosxzfIF63ES+hhkpVEUvzDYJyuMWZgGjFNV/X+cp WhdIpMySdNWSd9Qc+YO4bRC96XecrAvLT3pULuQVrWohTZtep3zsgS3jJW7ilD+w g1oa4A== -----END CERTIFICATE----- httpretty-0.0.6/testdata/certold.pem 0000664 0000000 0000000 00000002361 13725530515 0017544 0 ustar 00root root 0000000 0000000 -----BEGIN CERTIFICATE----- MIIDeDCCAmACCQCElHz7U8/oHjANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJV UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEa MBgGA1UECgwRWW91ciBPcmdhbml6YXRpb24xEjAQBgNVBAsMCVlvdXIgVW5pdDES MBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMDExOTEwMzE1NloXDTMwMDExNjEwMzE1 NlowfjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM DU1vdW50YWluIFZpZXcxGjAYBgNVBAoMEVlvdXIgT3JnYW5pemF0aW9uMRIwEAYD VQQLDAlZb3VyIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcN AQEBBQADggEPADCCAQoCggEBAL8t4Wx+PMt7EacmWC6aIufg9mtT32H/su1nON6V j4yaoKtRNx2gvxkVL9SoW2ooS8E+5vl/VI/o9FAzwetLCL6qa+TAdu+DSmdxTGFF fVVHpw4yvLvbPfFoCCgd/LkFZZBT+gvb7EikUT5RjIRsCmIL6+ZvOGzkOzEOlHi7 itJML3MvecpQWS0zyRbbwA1M0jUHeq/6Pt7PPbw94T6IHRFC+iJg4Wuz6IjvBUO9 41mUUF0f9qKHYmctjf2RngYRG025j3yzQDfQXfc0NVvWvk2M+WM4701Wvep8wjtO wQQ42bWD1FoytVGYbuYmu+TaovNYiYwr9vXXS/KIR5lwWB8CAwEAATANBgkqhkiG 9w0BAQsFAAOCAQEAqRizyUM1ja5MZHfh6dbPtFIrQcinhHVPvOs8qpa91D6LAhXb i5Yd/oVJwDsez562myAu04uMg3vl5vv2fuL3HA34aqPigdA7ITFiJQdwTawKjlWc Wgu6MAP17G8AcKpdqtVETU4dpkBRtVhg0CXShpWsylmE0h5ig7uibIRRY0YoT44x Bp5A8zxmEV3mO9qfx4gqsGJOlt8oFaYvr0BAemSb5lEg92hBEbaEewTDbWDV0u/C KxP8ndYb/EYitMEh5MFWR1/PPZsHBJbWbSTjJYXWeq8fizx+TW7UVxNgb9FvsGv2 9nql/a9eUSRSv63AORTPbPAMvUr6UNHGfERAPw== -----END CERTIFICATE----- httpretty-0.0.6/testdata/key-client.pem 0000664 0000000 0000000 00000003250 13725530515 0020152 0 ustar 00root root 0000000 0000000 -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC54gG9rMrVioL9 i3sHSkhE1iihNKAjRd+WS6iG60wNl7PeQcZvQhgf1d9/tZDyIM5mP0XaCRXxfRAH JeXDqjeLkGyqPPRNFgvUkzJDnKisZ7cPANqfWHJZz/qQF+ePvAnZiBEhp+9BXUfj iAHIMglvwn3W3s54d+0VDzmgZp1ha1LG/iU2MkHDpcNM/C8edtd9UmGG0mIHS0H1 JpNfuRNjPeFdCVvsi0wOZpIDWqzKiyCbro0IakxXStMIFwwpFEGgxkOytpJeqjdn EklTXxb0gxCthB9WKszzRsQfZ5UVW9guhYfp/a49cbcMO//qM3E6V/Pff9bLjkNL yC0ggi2BAgMBAAECggEACjjXh6q87MlVMsQ828XF+6MbUOIn/EiXZxh6CBFgeU7i YVKUqwGjefy08bz2X5pWP2EzYi4lusX536rB2+S8cTxb/XCkrqiLtgDyPq2ayQBb HMQbQbAHedDqIopt/YWFtSS6bHNjwOB0V5rfHjdCNZcofGx8RjuyGfpgXOXHudeo 5FTs+EBvuMRTOqxL6Pv8t5Q5761zJVZOyiUv5HzunXKPARHp+Y/si+6vwCrnShDq 0tDVX/zbZ3hkGrKMq0MvVkhbH5gil9BjcPmWkTEjfWuXuR8wB15oXltvWJPriv7f ILlKQxBmVbIfqjJvh7ShKUO3Cc8rV4KLe1bzUtgKkQKBgQD28bWRKX8OjXK1zwy1 +FOfiv7gBnt4avw1QCFMCBewu5RE5pAK6eMKE6Ef+a7vY44MkaGMzAu0mrKGMpP1 jjT8Yg2BoB9lMDrFIMA5rfhwJY7n1/qA5xIeCJDB5EZExQN4ql6BVETH/MIe/TtB m5DrTqhULMZfB+QcJIZSSY9GywKBgQDAsw8XhICghAwgmYeVL84kqBoBFPuBtKP+ wKvq4aiNY6wi4c8w8OgJcrpjD3Lz4tqisNvVRifMj7eVor+nGQkjC5mwLREyTKAV 1gQ+C7VOi/braVylEkwhFQ9jZvAmn7rQoNHb8L0P3um+ZKkb6uTym3/eYeAHDxvg RXx69fbHYwKBgB2gvG8RMnxVfjjQAa9nfuj6bUAFpxS4iU/+RMBxjB4ZM13c59VX YHUaC8/hThrMsANUCbTx2kmt8dNmCBiDGlpZjVNLGdkzIyn5lvaUp+UUrIOmhxim IKdX0b5hnAiuNo9oqXQM3z+7VLMRIOXrO0TwKAQJZzeJo9W4kCEZUEZnAoGBAJ08 foAOGnbfyJWBMWTGUUsP78gaOu8nWvmwdZd+8m4MepUr9EhXCr9K4lOac44V+Zju /zITwL3mN0LePcw3XYE/IfTjkTid1bJ7o5KNMzAYfS6yFmqLd5s2+AuAH00k4OcD kroIwfyFQ+2bbXHeRVrBD6GB869O4MwrZttegDNJAoGAWEvoSRcD0+n16vAN9fk7 tDdXAEcixH4jFbGlmsys+jrF2mndGRA1J40kjMW7H4xnF6b2ruXQkJtNcNN3Mvzy dwGkcsj+EcHg3AVxi7XrKgmyt+B34svNuB0uQxvshNflfvMmMdfm2GGngOsxp7rR hWJUNRVtNWmH+W0jGjuDFME= -----END PRIVATE KEY----- httpretty-0.0.6/testdata/key.pem 0000664 0000000 0000000 00000003250 13725530515 0016676 0 ustar 00root root 0000000 0000000 -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCvoOFz4m47kl5m w8In6LFC1Tanmo1vG4SscoU9XCg4Pht33qXE3CGOTQuNKtNmVVO4P1eCCjd8fwaR r0V04Wg/RK+AhneGxhMuQlebJxEEE4e4AoFyJzYeqcm6HLdKD2SLP9icajOUnF5Z erYns32sU34/htSqA8jdDBNoND0kPwCckvYGQu875n2V1BdJLEYAyv1oOPG9Ec45 nkyBApv9102WxICMf35O5XOKcegkp1g75D/ModNGNJ49k7ZLAajbRq5jkToo+u8L DkGmdavPO625bE7Roo24fAjUTVD/mYKAKzbqDtsrDOfyq8LMcH0Br/29vjCb/jKo e4e5A+j3AgMBAAECggEANrpFRt06SGn17MP3joQeKJtUKqoohITotOwCxPogtlX0 LUg+E7gc5MDxZo3/zhWsvu9OD4GrhKn4nBEn7aIH4B9BKSW9vUuf0nxt3DUyQjjr w9VUDQRXAvsZl1s3amadiB7fGu6lIBwR8oQgmwJ9mONzpcwYHNqNDwSiT4hnvRE4 RjQscjrqKYDkXekpk+3uPom3e5+UZXL0VoqjsB3PGN8xC3H8VFPcolPVfeKBhcxy zAOxvBfIyzKcooTtUn8UpCCqhCh+Ak/wHi08FptsSWj+FiWaE/d2OYXl3al+w4Tf CQApuaFcfsgzCtVeGDgPH0JIJKgLg/QLy9Z8x+zr8QKBgQDlw735Eb1Aszt7phJm y7/VE/FW3RwD0gyfr9kAlxxWjWwMfsEjmg4+A/bNbf5/G4OYmD7dZy3iF7pD/aQi kLwYgb04VVxz1xEjbBfaZhe1MBJKHQHkpBtKGBDPi1GYu2dFDkh6vqQX1/mvIfvn +B7I2BOvL9Fw37RvblbiJ2JHxQKBgQDDrq6YpBo4+H8cICyy6HuhyF9UMYVf+mwG lNCTW0bhMl1XRg4/KKS8JB3PR//KTq/c6pc/WYZu81FdEY9bRmN1lXcgTWXC5nWp P0ZNISJ71q2Lbz3cW0npvBh/Q4abmPdEHYgk+A7Z4vlTZw7DETb2FS9n6hvHrtsG 8gKkiSk9iwKBgAMuCVQIHdFmaZ1VeA26JiaBxyZHmxqmboxLN7qdXMQJ4wPtQSkH +ch77496RTpnHBQhj0UrJ2RopahJO1tLG39PVFoSPFxSDqep2E6qeQuF5crmyd7r MoF9AcaNjAyME2rOPsyMFONLluYIl17nfS2UZ/lVtRVV0z5zjXpFx0NtAoGAL2Wm QK6u81GtaCCa8xLAr2UbQgdkqOS9ObLd+nNHbdCHL1Z2qPGtRSzyU3y7BkOc8UOZ Muz6VPF2qbZRJOiduqNjYV2d4mFz6nS7EH+QHLLZAkcFktRByO2YeWrftdyNN+B3 U40J+9iwT3VM7A7FY0GqY98er3U49Cu2XCgk5xUCgYByAmWl2Z9J7HrsqIT695c2 R5AKmwhIIsF5UnbPPfmwAbgdxY0ZZE/X3Ec4WF9UlXOZMdOyZNAuFaPEf6jCaB5V 1uMGsLtVr4L+tHz+Fh+8EM36otCShyWq9PIE3dDXtkFVxAj4EMW/DamTQfqyj74d yClFOSq7k1xxrC1dvf1V5w== -----END PRIVATE KEY----- httpretty-0.0.6/testdata/key_example.pem 0000664 0000000 0000000 00000003250 13725530515 0020411 0 ustar 00root root 0000000 0000000 -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqQwATbK3/Eb83 1biFVrLEJaOvSJ1BDLpGiKt2nyWTuvAfqF5BkUETJm7KyickAD9oQC3eX33XhQzC UDzSj1R5Nz4I2qc/qgrTZcGryRsvBhBhKnWtScMbYaKh6sVUT2yL+UhGS3lzxdjm hwQoJ2eH4RPO7SjXwKUAVjATLAloC5RD3EWl76Ohqo6LLUOMBz/Xgw26gGFSmIMu HBOtqsxfSenMQunmd3yObBiw3dhGwcwnsdt9w39Y0/jvkTbyj7kwzvGH2EO44p5E GVHPSNOhH+kVUr7VoVpFOjPsCfYf81RiBUseozAd935PfXNc7L6d8S9dhaeqJovr 7v1baSsRAgMBAAECggEADGW8h62OLdh49/PT78GUWrvy4zyCVs46chBZi9WiwtMF 0QhNdLDC8EYIIzP9DZ4G/+xMarjBTQQfHbcB9sMA/6KKHdLuArC7ARGTvJJ0LERg xPJ2hxur3T6KvQd/PthZqweHv7aXLVrmpEKIhvP3kelNq++Q3cTlPtUHwx2dwbmX seDtsUIOStdeijJBchqQcxwft5KeXQDYxJSK5KzAmLxSVnyDdU+sXbwOtXlw252d /NHAC+U01dNlxsYoeguTrkqOX52DI5awAoykB3cStWQiPb1bN+0Qa6ORvXUX/wKD TYT0yhdkxu/OmmWy+wVNIMS9uIRP8bPOwVyFihIdUQKBgQDJgTYfbE8G6+uOklQw 4ohI8yXDsQT7wGIZuBeFwQO4t/k5B5ymE9tZ169ka7KHM3I5OBpC3eHpQiaWvyOH 1psjdEIehhnIO/Hm1rELOlRFWKLHBuFLoqxvDQjUddWMhddZvJetBwhDiX4AzL7p cnAyeVclIn43Fk277ETHy63L6wKBgQDYTr1Y9uep91HqpOcKRiR8tA1P2NxJDTZR IucvOiMJ6tw4cys0D2JSfaKAQigv+P+pjRyXrYwkaRDB/gEk587wntE9wPIMs/2c 95Cxp3Uwsvwv52kHn4WH1DJfMSvASe84qNkANWw0jjhjbXvxhZ3M1i2KfCdjYTfZ YLI1NTIR8wKBgQCyz4JjqA0Iq1nAjoE/UAZ4FawxV2iArltfT0kwW/Mde8Qgo2yS w5QmyYrOpfMqnrCBrhM/uv25rAXqR3sUE5Bfic8SnxVJ5kfm/CTnPb+COgFYc/aA 074IXZy0TExQAoTzELPXyyG+LMgvlYDkT7TYVWzLeyxdXeFlHWh7k3aKOQKBgCQM a03qSA1xZDuAo+h4bBhEQXuvHncmNokrEfAy9ifu9iiKOQcCEVbCDVTmsZ/dFW6C T+OPTq26vMo3tKUb5McBEMoD39LyJDAGqhyRVdx518F8BWr50N0kJgjrPula6P0+ VnvMa24OzaL0WhWUOQosH4bWzhGn4BDgJpLrfJ61AoGACFkdXnHBgdU8lvbf6TIX w6EtfkjPikSMyFojTvoUtJQuMHldUocuYwpuHlfp0ubqmaqSrRbFQwxpE5Qe19gQ yvh6jsNAChuhQACbJv29PzO65J3zgqPWjQOz+2LVdx1YkXgK+IF1xuDC8DCBmJCn v7mn8Euw5b3Y4iZ47Kd9ZIw= -----END PRIVATE KEY----- httpretty-0.0.6/testdata/keyold.pem 0000664 0000000 0000000 00000003250 13725530515 0017375 0 ustar 00root root 0000000 0000000 -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/LeFsfjzLexGn JlgumiLn4PZrU99h/7LtZzjelY+MmqCrUTcdoL8ZFS/UqFtqKEvBPub5f1SP6PRQ M8HrSwi+qmvkwHbvg0pncUxhRX1VR6cOMry72z3xaAgoHfy5BWWQU/oL2+xIpFE+ UYyEbApiC+vmbzhs5DsxDpR4u4rSTC9zL3nKUFktM8kW28ANTNI1B3qv+j7ezz28 PeE+iB0RQvoiYOFrs+iI7wVDveNZlFBdH/aih2JnLY39kZ4GERtNuY98s0A30F33 NDVb1r5NjPljOO9NVr3qfMI7TsEEONm1g9RaMrVRmG7mJrvk2qLzWImMK/b110vy iEeZcFgfAgMBAAECggEBAIs2sr9ZUZXu4VTxZCdTUHW/6FERe0oWc8nSb6QODsEZ XEREWLk3c9ClD/ZwPlkYTMnEr1chdIdy4G2CswRO2GhXG0gxKqqQ1V5sL21pt7Gh ArIhGjRFm16uHbpw07Y7itDFhgCavf3Lwel6YrOPJSRuf/KGLPWGYOABOPaNwrIN SouNd/8mxBc57vbrgx1bUFS7vt09vqGKsQYDxvOP1a5UPvwqqq9H4lEx9LrVwAte Pm6gDrjPbADfSCBWGZdOwnDiY59HY/R4la5d2d6sGoAa73vQZCDJEtpuDh0LOFOS IeTPP+erAK71y5p07aplMNPrCUtk/4f3Gd/WKAPiLoECgYEA89b9nR8tzkn64BXZ hK+gB1OFTNc9ACzIQkh7/Z88z6txqQ1QOPOA8TJXPs1bNZg1soTQ/nwMAEN/SbSq O2Z6WMAnh0j/ATIs0rYTO/sS7ZjV67tXRWzkEei9P6Wm3NJ05DfcyvbBt1ScrEjB JN7D4/r1sACP+fFam/Swu+Yj1kECgYEAyLaXpXzzHZRZ3Kq3WKJaB08ydQlERoaX RJMLAS17LWS8BNDjBl3IQwnnTR4zB0TELgQszWRsMrOOCGDhid8owt21XogA+yi9 Z4XsR4nKh8m64Vp8d3DVAxJo+c+OYdfhA8rWptHvfa4SeZbjGPoybgyjFgUhYI63 Ty1dMqGqVl8CgYEAhjb7J8Xmp5qO7VL5hJBKzF2LjM0YdYUwwVM2dFZ22XPrvvpm AsL9YUWtQhM0th5OyDFU/A55aJe+c2pvHPz+MOWrnEpwmk7s3xp7IdPECmXKsdNP aRZTvwvVRzg9zWRGFOwuqsUBwZBgIHB3Z3z6Y/1ZyIO2vAO+NQONWA+IAEECgYBl yQsgVjwoDPqBSGXQYgzL1iLdbUSdi1Wc5gDXqQvlWkdrHc9zhA2xyYzt89mm3v2p 5F4gDsQ79giaQR8/Ptc58xsuBESTGfbrT+Qh50O5FtlZvPyPyb2MYEKyJMqs3cBz nuK6GI6eKq+dz6H9Iax/WJM/8HwbrmRRl8zCh2+NewKBgEOQ5GU/kayXBWssdrN1 VeyNYI9Gd8/Rr65vN8xxYoZabi/EC08daQBka+f73AShZboY0JOfAQNKp3R33Gf1 2PLP52mfQ4zeuhAcqOnX0NCrSBXFzx/+1kfAnKP50StfV0Ke4mdeqtFTZ9W+XsUC IMbePLH0bQhynrKsJWiGXtwv -----END PRIVATE KEY----- httpretty-0.0.6/testdata/petition.golden 0000664 0000000 0000000 00000023166 13725530515 0020440 0 ustar 00root root 0000000 0000000 Pétition 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. À MM. les Membres de la Chambre des Députés. « Messieurs, « 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. « 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. « 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. « 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. « 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. « 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 ? « 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. « 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. « 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. « 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. « 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. « 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. « 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. « 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. « Nous direz-vous que, si nous gagnons à cette protection, la France n’y gagnera point, parce que le consommateur en fera les frais ? « Nous vous répondrons : « 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. « 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. « 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. « 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 ? « 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é. « 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. « 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. « 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. « 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. « 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 ? » httpretty-0.0.6/tls.go 0000664 0000000 0000000 00000003717 13725530515 0014733 0 ustar 00root root 0000000 0000000 package httpretty // A list of cipher suite IDs that are, or have been, implemented by the // crypto/tls package. // See https://www.iana.org/assignments/tls-parameters/tls-parameters.xml // See https://github.com/golang/go/blob/c2edcf4b1253fdebc13df8a25979904c3ef01c66/src/crypto/tls/cipher_suites.go var tlsCiphers = map[uint16]string{ // TLS 1.0 - 1.2 cipher suites. 0x0005: "TLS_RSA_WITH_RC4_128_SHA", 0x000a: "TLS_RSA_WITH_3DES_EDE_CBC_SHA", 0x002f: "TLS_RSA_WITH_AES_128_CBC_SHA", 0x0035: "TLS_RSA_WITH_AES_256_CBC_SHA", 0x003c: "TLS_RSA_WITH_AES_128_CBC_SHA256", 0x009c: "TLS_RSA_WITH_AES_128_GCM_SHA256", 0x009d: "TLS_RSA_WITH_AES_256_GCM_SHA384", 0xc007: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", 0xc009: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 0xc00a: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 0xc011: "TLS_ECDHE_RSA_WITH_RC4_128_SHA", 0xc012: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", 0xc013: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 0xc014: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 0xc023: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 0xc027: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 0xc02f: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 0xc02b: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 0xc030: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 0xc02c: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 0xcca8: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 0xcca9: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA25", // TLS 1.3 cipher suites. 0x1301: "TLS_AES_128_GCM_SHA256", 0x1302: "TLS_AES_256_GCM_SHA384", 0x1303: "TLS_CHACHA20_POLY1305_SHA256", // TLS_FALLBACK_SCSV isn't a standard cipher suite but an indicator // that the client is doing version fallback. See RFC 7507. 0x5600: "TLS_FALLBACK_SCSV", } // List of TLS protocol versions supported by Go. // See https://github.com/golang/go/blob/f4a8bf128364e852cff87cf404a5c16c457ef8f6/src/crypto/tls/common.go var tlsProtocolVersions = map[uint16]string{ 0x0301: "TLS 1.0", 0x0302: "TLS 1.1", 0x0303: "TLS 1.2", 0x0304: "TLS 1.3", }