From 53b79cacd6708a1a48aabf8b7d6f4c4bb6be6a1d Mon Sep 17 00:00:00 2001 From: Fabricio Date: Fri, 8 Mar 2024 20:21:36 -0300 Subject: [PATCH] Huge code simplification --- .github/workflows/build.yml | 4 +- README.md | 47 ++- capture.go | 139 --------- config.go | 28 -- dashboard.html | 595 ++++++++++-------------------------- go.mod | 2 +- main.go | 340 ++++++--------------- main_test.go | 175 +++-------- 8 files changed, 329 insertions(+), 1001 deletions(-) delete mode 100644 capture.go delete mode 100644 config.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1a2da0a..949ed96 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,10 +7,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.16 + - name: Set up Go uses: actions/setup-go@v1 with: - go-version: 1.16 + go-version: 1.22 id: go - name: Check out code into the Go module directory diff --git a/README.md b/README.md index 0c59440..bb7516d 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,17 @@ proxying the response back to the client, while showing them in a dashboard. ## Running - ./capture -url=https://example.com/ - +```sh +./capture -url=https://example.com/ +``` #### Settings | param | description | |--------------|-------------| -| `-url` | **Required.** Set the url you want to proxy | +| `-url` | **Required.** Set the url to proxy | | `-port` | Set the proxy port. Default: *9000* | | `-dashboard` | Set the dashboard port. Default: *9001* | -| `-captures` | Set how many captures to show in the dashboard. Default: *16* | ## Using @@ -40,33 +40,30 @@ To access the dashboard go to `http://localhost:9001/` ## Building -Manually: +### For manual build - git clone --depth 1 https://github.com/ofabricio/capture.git - cd capture - go build +```sh +git clone --depth 1 https://github.com/ofabricio/capture.git +cd capture +go build +``` -Via docker: +### For building with docker - git clone --depth 1 https://github.com/ofabricio/capture.git - cd capture - docker run --rm -v $PWD:/src -w /src -e GOOS=darwin -e GOARCH=amd64 golang:alpine go build +```sh +git clone --depth 1 https://github.com/ofabricio/capture.git +cd capture +docker run --rm -v $PWD:/src -w /src -e GOOS=darwin -e GOARCH=amd64 golang:alpine go build +``` Now you have an executable binary in your directory. -**Note:** change `GOOS=darwin` to `linux` or `windows` to create an executable for your corresponding Operating System. - -## Plugins - -Put [plugin](https://golang.org/pkg/plugin/) files in the current directory. -They are loaded sorted by filename on startup. +**Note:** set `GOOS=darwin` to `linux` or `windows` to create an executable for the corresponding Operating System. -Plugins must export the following function: +### For running straight from docker -```go -func Handler(proxy http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - proxy(w, r) - } -} +```sh +git clone --depth 1 https://github.com/ofabricio/capture.git +cd capture +docker run --rm -v $PWD:/src -w /src golang:alpine apk add ca-certificates && go run main.go -url=http://example.com ``` diff --git a/capture.go b/capture.go deleted file mode 100644 index f5f11e8..0000000 --- a/capture.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "net/http" - "strconv" - "sync" - "time" -) - -var captureID int - -// CaptureService handles captures. -type CaptureService struct { - items []Capture - mu sync.RWMutex - maxItems int - updated chan struct{} // signals any change in "items". -} - -// Capture is our traffic data. -type Capture struct { - ID int - Req Req - Res Res - // Elapsed time of the request, in milliseconds. - Elapsed time.Duration -} - -type Req struct { - Proto string - Method string - Url string - Path string - Header http.Header - Body []byte -} - -type Res struct { - Proto string - Status string - Code int - Header http.Header - Body []byte -} - -// CaptureInfo is the capture info shown in the dashboard. -type CaptureInfo struct { - Request string `json:"request"` - Response string `json:"response"` - Curl string `json:"curl"` -} - -// DashboardItem is an item in the dashboard's list. -type DashboardItem struct { - ID int `json:"id"` - Path string `json:"path"` - Method string `json:"method"` - Status int `json:"status"` - - Elapsed time.Duration `json:"elapsed"` -} - -// NewCaptureService creates a new service of captures. -func NewCaptureService(maxItems int) *CaptureService { - return &CaptureService{ - maxItems: maxItems, - updated: make(chan struct{}), - } -} - -// Insert inserts a new capture. -func (s *CaptureService) Insert(capture Capture) { - s.mu.Lock() - defer s.mu.Unlock() - - captureID++ - capture.ID = captureID - s.items = append(s.items, capture) - if len(s.items) > s.maxItems { - s.items = s.items[1:] - } - s.signalsUpdate() -} - -// Find finds a capture by its ID. -func (s *CaptureService) Find(captureID string) *Capture { - s.mu.RLock() - defer s.mu.RUnlock() - - idInt, _ := strconv.Atoi(captureID) - for _, c := range s.items { - if c.ID == idInt { - return &c - } - } - return nil -} - -// RemoveAll removes all the captures. -func (s *CaptureService) RemoveAll() { - s.mu.Lock() - defer s.mu.Unlock() - - s.items = nil - s.signalsUpdate() -} - -// DashboardItems returns the dashboard's list of items. -func (s *CaptureService) DashboardItems() []DashboardItem { - s.mu.RLock() - defer s.mu.RUnlock() - - metadatas := make([]DashboardItem, len(s.items)) - for i, capture := range s.items { - metadatas[i] = DashboardItem{ - ID: capture.ID, - Path: capture.Req.Path, - Method: capture.Req.Method, - Status: capture.Res.Code, - Elapsed: capture.Elapsed, - } - } - return metadatas -} - -// signalsUpdate fires an update signal. -func (s *CaptureService) signalsUpdate() { - close(s.updated) - s.updated = make(chan struct{}) -} - -// Updated signals any change in this service, -// like inserting or removing captures. -func (s *CaptureService) Updated() <-chan struct{} { - s.mu.RLock() - defer s.mu.RUnlock() - - return s.updated -} diff --git a/config.go b/config.go deleted file mode 100644 index 73b5647..0000000 --- a/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "flag" -) - -// Config has all the configuration parsed from the command line. -type Config struct { - TargetURL string - ProxyPort string - DashboardPort string - MaxCaptures int -} - -// ReadConfig reads the arguments from the command line. -func ReadConfig() Config { - targetURL := flag.String("url", "https://jsonplaceholder.typicode.com", "Required. Set the url you want to proxy") - proxyPort := flag.String("port", "9000", "Set the proxy port") - dashboardPort := flag.String("dashboard", "9001", "Set the dashboard port") - maxCaptures := flag.Int("captures", 16, "Set how many captures to show in the dashboard") - flag.Parse() - return Config{ - TargetURL: *targetURL, - ProxyPort: *proxyPort, - MaxCaptures: *maxCaptures, - DashboardPort: *dashboardPort, - } -} diff --git a/dashboard.html b/dashboard.html index 30381fd..1479867 100644 --- a/dashboard.html +++ b/dashboard.html @@ -1,441 +1,170 @@ - + - - - - - - Capture - + + + capture + + + + + + + - - -
- -
-
- -
-
-
- {{item.method}} - ‎{{item.path}}‎ - {{item.elapsed}}ms - {{item.status == 999 ? 'failed' : - item.status}} -
-
-
- -
-
- - - -
-
-
req
-
{{selectedItem.request}}
-
-
- -
-
- -
-
-
res
-
{{selectedItem.response}}
-
-
- -
-

Waiting for requests on http://localhost:{{proxyPort}}/
- Proxying {{targetURL}} -

-
- -
- - + + +
+ +
+
+ +
+
+
+ +
+
+ + + +
+ +
+ +
+
+ +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/go.mod b/go.mod index d33b670..113fe6d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/ofabricio/capture -go 1.16 +go 1.22.0 diff --git a/main.go b/main.go index 75b4709..8b6b0b6 100644 --- a/main.go +++ b/main.go @@ -1,298 +1,150 @@ package main import ( + "bufio" "bytes" - "compress/gzip" _ "embed" "encoding/json" + "flag" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "os" - "path" - "path/filepath" - "plugin" - "sort" "strings" "time" ) -// StatusInternalProxyError is any unknown proxy error. -const StatusInternalProxyError = 999 - //go:embed dashboard.html -var dashboardHTML []byte +var dashHTML []byte func main() { - cfg := ReadConfig() - - fmt.Printf("\nListening on http://localhost:%s", cfg.ProxyPort) - fmt.Printf("\nDashboard on http://localhost:%s", cfg.DashboardPort) - fmt.Println() - srv := NewCaptureService(cfg.MaxCaptures) - hdr := NewRecorderHandler(srv, NewPluginHandler(NewProxyHandler(cfg.TargetURL))) + proxyURL := flag.String("url", "https://jsonplaceholder.typicode.com", "Required. Set the url to proxy") + proxPort := flag.String("port", "9000", "Set the proxy port") + dashPort := flag.String("dashboard", "9001", "Set the dashboard port") + flag.Parse() - go func() { - fmt.Println(http.ListenAndServe(":"+cfg.DashboardPort, NewDashboardHandler(hdr, srv, cfg))) + URL, err := url.Parse(*proxyURL) + if err != nil { + fmt.Println(err) os.Exit(1) - }() - fmt.Println(http.ListenAndServe(":"+cfg.ProxyPort, hdr)) -} - -func NewDashboardHandler(h http.HandlerFunc, srv *CaptureService, cfg Config) http.Handler { - router := http.NewServeMux() - router.HandleFunc("/", NewDashboardHTMLHandler()) - router.HandleFunc("/conn/", NewDashboardConnHandler(srv, cfg)) - router.HandleFunc("/info/", NewDashboardInfoHandler(srv)) - router.HandleFunc("/clear/", NewDashboardClearHandler(srv)) - router.HandleFunc("/retry/", NewDashboardRetryHandler(srv, h)) - return router -} - -// NewDashboardConnHandler opens an event stream connection with the dashboard -// so that it is notified everytime a new capture arrives. -func NewDashboardConnHandler(srv *CaptureService, cfg Config) http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { - if _, ok := w.(http.Flusher); !ok { - http.Error(w, "streaming not supported", http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") - w.Header().Set("Connection", "keep-alive") - - sendEvent := func(event string, data interface{}) { - jsn, _ := json.Marshal(data) - fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, jsn) - w.(http.Flusher).Flush() - } - - sendEvent("config", cfg) - - for { - sendEvent("captures", srv.DashboardItems()) - - select { - case <-srv.Updated(): - case <-req.Context().Done(): - return - } - } } -} -// NewDashboardClearHandler clears all the captures. -func NewDashboardClearHandler(srv *CaptureService) http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - srv.RemoveAll() - rw.WriteHeader(http.StatusOK) - } -} + proxy, mux := NewProxy(URL, make(chan Capture)) -// NewDashboardHTMLHandler returns the dashboard html page. -func NewDashboardHTMLHandler() http.HandlerFunc { - return func(w http.ResponseWriter, req *http.Request) { + fmt.Printf("\nListening on http://localhost:%s", *proxPort) + fmt.Printf("\nDashboard on http://localhost:%s", *dashPort) + fmt.Printf("\n\n") - // This redirect prevents accessing the dashboard page from paths other - // than the root path. This is important because the dashboard uses - // relative paths, so "/retry/" would become "/something/retry/". - if req.URL.Path != "/" { - http.Redirect(w, req, "/", http.StatusTemporaryRedirect) - return - } - - w.Header().Add("Content-Type", "text/html") - w.Write(dashboardHTML) - } + go http.ListenAndServe(":"+*dashPort, mux) + fmt.Println(http.ListenAndServe(":"+*proxPort, http.HandlerFunc(proxy))) } -// NewDashboardRetryHandler retries a request. -func NewDashboardRetryHandler(srv *CaptureService, next http.HandlerFunc) http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - id := path.Base(req.URL.Path) - capture := srv.Find(id) +func NewProxy(URL *url.URL, captures chan Capture) (http.HandlerFunc, *http.ServeMux) { - // Create a new request based on the current one. - r, _ := http.NewRequest(capture.Req.Method, capture.Req.Url, bytes.NewReader(capture.Req.Body)) - r.Header = capture.Req.Header + proxy := func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(rw, r) - } -} + r.Host = URL.Host + r.URL.Host = URL.Host + r.URL.Scheme = URL.Scheme -// NewDashboardInfoHandler returns the full capture info. -func NewDashboardInfoHandler(srv *CaptureService) http.HandlerFunc { - return func(rw http.ResponseWriter, req *http.Request) { - id := path.Base(req.URL.Path) - capture := srv.Find(id) - rw.Header().Add("Content-Type", "application/json") - json.NewEncoder(rw).Encode(dump(capture)) - } -} + var reqBody bytes.Buffer + r.Body = io.NopCloser(io.TeeReader(r.Body, &reqBody)) -// NewPluginHandler loads plugin files in the current directory. -// They are loaded sorted by filename. -func NewPluginHandler(next http.HandlerFunc) http.HandlerFunc { - ex, err := os.Executable() - if err != nil { - fmt.Println("error: could not get executable:", err) - return next - } - exPath := filepath.Dir(ex) - files, err := ioutil.ReadDir(exPath) - if err != nil { - fmt.Println("error: could not read directory:", err) - return next - } - for _, file := range files { - if file.IsDir() { - continue - } - if strings.HasSuffix(file.Name(), ".so") { - fmt.Printf("Loading plugin '%s'\n", file.Name()) - p, err := plugin.Open(exPath + "/" + file.Name()) - if err != nil { - fmt.Println("error: could not open plugin:", err) - os.Exit(1) - } - fn, err := p.Lookup("Handler") - if err != nil { - fmt.Println("error: could not find plugin Handler function:", err) - os.Exit(1) - } - pluginHandler, ok := fn.(func(http.HandlerFunc) http.HandlerFunc) - if !ok { - fmt.Println("error: plugin Handler function should be 'func(http.HandlerFunc) http.HandlerFunc'") - os.Exit(1) - } - next = pluginHandler(next) - } - } - return next -} - -// NewRecorderHandler records all the traffic data. -func NewRecorderHandler(srv *CaptureService, next http.HandlerFunc) http.HandlerFunc { - return func(rw http.ResponseWriter, r *http.Request) { - - // Save req body for later. - - reqBody := &bytes.Buffer{} - r.Body = ioutil.NopCloser(io.TeeReader(r.Body, reqBody)) - - rec := httptest.NewRecorder() - - // Record Roundtrip. + res := httptest.NewRecorder() start := time.Now() - - next.ServeHTTP(rec, r) - + httputil.NewSingleHostReverseProxy(URL).ServeHTTP(res, r) elapsed := time.Since(start).Truncate(time.Millisecond) / time.Millisecond - resBody := rec.Body.Bytes() - - // Respond to client with recorded response. - - for k, v := range rec.Header() { - rw.Header()[k] = v + for k, v := range res.Header() { + w.Header()[k] = v } - rw.WriteHeader(rec.Code) - rw.Write(resBody) - - // Save req and res data. - - req := Req{ - Proto: r.Proto, - Method: r.Method, - Url: r.URL.String(), - Path: r.URL.Path, - Header: r.Header, - Body: reqBody.Bytes(), + w.WriteHeader(res.Code) + w.Write(res.Body.Bytes()) + + r.Body = io.NopCloser(bytes.NewReader(reqBody.Bytes())) + + dumpReq, _ := httputil.DumpRequest(r, true) + dumpRes, _ := httputil.DumpResponse(res.Result(), true) + + select { + case captures <- Capture{ + Verb: r.Method, + Path: r.URL.Path, + Status: res.Result().Status, + Group: res.Code / 100, + Req: string(dumpReq), + Res: string(dumpRes), + Elapsed: elapsed, + Curl: curl(r.Method, r.URL.String(), r.Header, reqBody.Bytes()), + }: + default: } - res := Res{ - Proto: rec.Result().Proto, - Status: rec.Result().Status, - Code: rec.Code, - Header: rec.Header(), - Body: resBody, - } - srv.Insert(Capture{Req: req, Res: res, Elapsed: elapsed}) } -} -// NewProxyHandler is the reverse proxy handler. -func NewProxyHandler(URL string) http.HandlerFunc { - url, _ := url.Parse(URL) - proxy := httputil.NewSingleHostReverseProxy(url) - proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { - fmt.Printf("Uh oh | %v | %s %s\n", err, req.Method, req.URL) - rw.WriteHeader(StatusInternalProxyError) - fmt.Fprintf(rw, "%v", err) - } - return func(rw http.ResponseWriter, req *http.Request) { - req.Host = url.Host - req.URL.Host = url.Host - req.URL.Scheme = url.Scheme - proxy.ServeHTTP(rw, req) - } -} + mux := http.NewServeMux() -func dump(c *Capture) CaptureInfo { - req := c.Req - res := c.Res - return CaptureInfo{ - Request: dumpContent(req.Header, req.Body, "%s %s %s\n\n", req.Method, req.Path, req.Proto), - Response: dumpContent(res.Header, res.Body, "%s %s\n\n", res.Proto, res.Status), - Curl: dumpCurl(req), - } -} - -func dumpContent(header http.Header, body []byte, format string, args ...interface{}) string { - b := strings.Builder{} - fmt.Fprintf(&b, format, args...) - dumpHeader(&b, header) - b.WriteString("\n") - dumpBody(&b, header, body) - return b.String() -} + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "text/html") + w.Write(dashHTML) + }) + + mux.HandleFunc("/retry", func(w http.ResponseWriter, r *http.Request) { + rr, err := http.ReadRequest(bufio.NewReader(strings.NewReader(r.FormValue("req")))) + if err != nil { + fmt.Println("invalid request format:", err) + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + proxy(httptest.NewRecorder(), rr) + w.WriteHeader(200) + }) -func dumpHeader(dst *strings.Builder, header http.Header) { - var headers []string - for k, v := range header { - headers = append(headers, fmt.Sprintf("%s: %s\n", k, strings.Join(v, " "))) - } - sort.Strings(headers) - for _, v := range headers { - dst.WriteString(v) - } -} + mux.HandleFunc("/captures", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + for { + select { + case c := <-captures: + jsn, _ := json.Marshal(c) + fmt.Fprintf(w, "event: captures\ndata: %s\n\n", jsn) + w.(http.Flusher).Flush() + case <-r.Context().Done(): + return + } + } + }) -func dumpBody(dst *strings.Builder, header http.Header, body []byte) { - reqBody := body - if header.Get("Content-Encoding") == "gzip" { - reader, _ := gzip.NewReader(bytes.NewReader(body)) - reqBody, _ = ioutil.ReadAll(reader) - } - dst.Write(reqBody) + return proxy, mux } -func dumpCurl(req Req) string { +func curl(method, url string, head http.Header, body []byte) string { var b strings.Builder // Build cmd. - fmt.Fprintf(&b, "curl -X %s %s", req.Method, req.Url) - // Build headers. - for k, v := range req.Header { + fmt.Fprintf(&b, "curl -X %s %s", method, url) + // Build head. + for k, v := range head { fmt.Fprintf(&b, " \\\n -H '%s: %s'", k, strings.Join(v, " ")) } // Build body. - if len(req.Body) > 0 { - fmt.Fprintf(&b, " \\\n -d '%s'", req.Body) + if len(body) > 0 { + fmt.Fprintf(&b, " \\\n -d '%s'", body) } return b.String() } + +type Capture struct { + Verb string + Path string + Status string + Group int + Req string + Res string + Curl string + Elapsed time.Duration +} diff --git a/main_test.go b/main_test.go index 9227b83..c62089a 100644 --- a/main_test.go +++ b/main_test.go @@ -1,150 +1,67 @@ package main import ( - "bytes" "compress/gzip" "fmt" - "io" - "io/ioutil" "net/http" "net/http/httptest" - "strings" - "testing" + "net/url" ) -// Test the reverse proxy handler -func TestProxyHandler(t *testing.T) { - // given - tt := []TestCase{ - GetRequest(), - PostRequest(), - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - service := httptest.NewServer(http.HandlerFunc(tc.service)) - capture := httptest.NewServer(NewProxyHandler(service.URL)) - - // when - resp := tc.request(capture.URL) - - // then - tc.test(t, resp) - - resp.Body.Close() - capture.Close() - service.Close() - }) - } -} +func Example() { -type TestCase struct { - name string - request func(string) *http.Response - service func(http.ResponseWriter, *http.Request) - test func(*testing.T, *http.Response) -} + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Date", "Sun, 10 Mar 2024 01:05:03 GMT") + if r.URL.Path == "/say" { + w.Write([]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")) + } + if r.URL.Path == "/say/gzip" { + w.Header().Set("Content-Encoding", "gzip") + g := gzip.NewWriter(w) + g.Write([]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")) + g.Close() + } + })) -func GetRequest() TestCase { - msg := "hello" - return TestCase{ - name: "GetRequest", - request: func(url string) *http.Response { - res, _ := http.Get(url) - return res - }, - service: func(rw http.ResponseWriter, req *http.Request) { - fmt.Fprint(rw, string(msg)) - }, - test: func(t *testing.T, res *http.Response) { - body, _ := ioutil.ReadAll(res.Body) - if string(body) != msg { - t.Error("Wrong Body Response") - } - }, - } -} + u, _ := url.Parse(srv.URL) -func PostRequest() TestCase { - msg := "hello" - return TestCase{ - name: "PostRequest", - request: func(url string) *http.Response { - res, _ := http.Post(url, "text/plain", strings.NewReader(msg)) - return res - }, - service: func(rw http.ResponseWriter, req *http.Request) { - io.Copy(rw, req.Body) - }, - test: func(t *testing.T, res *http.Response) { - body, _ := ioutil.ReadAll(res.Body) - if string(body) != msg { - t.Error("Wrong Body Response") - } - }, - } -} + captures := make(chan Capture) -func TestDashboardRedirect(t *testing.T) { + proxy, _ := NewProxy(u, captures) + + // Test regular response. - // Given. - req, _ := http.NewRequest(http.MethodGet, "/something/", nil) rec := httptest.NewRecorder() + req := httptest.NewRequest("GET", "/say", nil) - // When. - NewDashboardHTMLHandler().ServeHTTP(rec, req) + go proxy(rec, req) + c := <-captures - // Then. - if rec.Code != http.StatusTemporaryRedirect { - t.Errorf("Wrong response code: got %d, want %d", rec.Code, http.StatusTemporaryRedirect) - } - if loc := rec.Header().Get("Location"); loc != "/" { - t.Errorf("Wrong redirect path: got '%s', want '/'", loc) - } -} + rec.Header().Del("Date") -func Example_dump() { - c := &Capture{ - Req: Req{ - Proto: "HTTP/1.1", - Url: "http://localhost/hello", - Path: "/hello", - Method: "GET", - Header: map[string][]string{"Content-Encoding": {"none"}}, - Body: []byte(`hello`), - }, - Res: Res{ - Proto: "HTTP/1.1", - Header: map[string][]string{"Content-Encoding": {"gzip"}}, - Body: gzipStr("gziped hello"), - Status: "200 OK", - }, - } - got := dump(c) - - fmt.Println(got.Request) - fmt.Println(got.Response) - fmt.Println(got.Curl) + fmt.Println("Test regular response") + fmt.Println(rec.Code == 200, rec.Body.String() == "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") + fmt.Println(c.Res == "HTTP/1.1 200 OK\r\nContent-Length: 56\r\nContent-Type: text/plain; charset=utf-8\r\nDate: Sun, 10 Mar 2024 01:05:03 GMT\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit.") - // Output: - // GET /hello HTTP/1.1 - // - // Content-Encoding: none - // - // hello - // HTTP/1.1 200 OK - // - // Content-Encoding: gzip - // - // gziped hello - // curl -X GET http://localhost/hello \ - // -H 'Content-Encoding: none' \ - // -d 'hello' -} + // Test gzip response. + + rec = httptest.NewRecorder() + req = httptest.NewRequest("GET", "/say/gzip", nil) + + go proxy(rec, req) + c = <-captures -func gzipStr(str string) []byte { - var buff bytes.Buffer - g := gzip.NewWriter(&buff) - io.WriteString(g, str) - g.Close() - return buff.Bytes() + rec.Header().Del("Date") + + fmt.Println("Test gzip response") + fmt.Println(rec.Code == 200, rec.Body.String() == "Lorem ipsum dolor sit amet, consectetur adipiscing elit.") + fmt.Println(c.Res == "HTTP/1.1 200 OK\r\nConnection: close\r\nDate: Sun, 10 Mar 2024 01:05:03 GMT\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit.") + + // Output: + // Test regular response + // true true + // true + // Test gzip response + // true true + // true }