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..c9b69bd 100644
--- a/dashboard.html
+++ b/dashboard.html
@@ -1,441 +1,172 @@
-
+
-
-
-
-
-
- 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}}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ↳ {{Elapsed}}ms {{Status}}
+
+
+
+
+
\ 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
}