Skip to content

Commit 6f5ace7

Browse files
committed
feat: support to set upstream proxy address
1 parent 8da0890 commit 6f5ace7

File tree

10 files changed

+118
-36
lines changed

10 files changed

+118
-36
lines changed

extensions/collector/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@ It will start a HTTP proxy server, and set the server address to your browser pr
1010

1111
`atest-collector` will record all HTTP requests which has prefix `/answer/api/v1`, and
1212
save it to file `sample.yaml` once you close the server.
13+
14+
## Features
15+
16+
* Basic authorization
17+
* Upstream proxy
18+
* URL path filter
19+
* Support save response body or not

extensions/collector/cmd/collect.go

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
package cmd
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
7+
"io"
68
"net/http"
9+
"net/url"
710
"os"
811
"os/signal"
912
"strings"
1013
"syscall"
1114

1215
"github.com/elazarl/goproxy"
16+
"github.com/elazarl/goproxy/ext/auth"
1317
"github.com/linuxsuren/api-testing/extensions/collector/pkg"
1418
"github.com/linuxsuren/api-testing/extensions/collector/pkg/filter"
1519
"github.com/spf13/cobra"
1620
)
1721

1822
type option struct {
19-
port int
20-
filterPath string
21-
output string
23+
port int
24+
filterPath []string
25+
saveResponseBody bool
26+
output string
27+
upstreamProxy string
28+
verbose bool
29+
username string
30+
password string
2231
}
2332

2433
// NewRootCmd creates the root command
@@ -31,8 +40,13 @@ func NewRootCmd() (c *cobra.Command) {
3140
}
3241
flags := c.Flags()
3342
flags.IntVarP(&opt.port, "port", "p", 8080, "The port for the proxy")
34-
flags.StringVarP(&opt.filterPath, "filter-path", "", "", "The path prefix for filtering")
43+
flags.StringSliceVarP(&opt.filterPath, "filter-path", "", []string{}, "The path prefix for filtering")
44+
flags.BoolVarP(&opt.saveResponseBody, "save-response-body", "", false, "Save the response body")
3545
flags.StringVarP(&opt.output, "output", "o", "sample.yaml", "The output file")
46+
flags.StringVarP(&opt.upstreamProxy, "upstream-proxy", "", "", "The upstream proxy")
47+
flags.StringVarP(&opt.username, "username", "", "", "The username for basic auth")
48+
flags.StringVarP(&opt.password, "password", "", "", "The password for basic auth")
49+
flags.BoolVarP(&opt.verbose, "verbose", "", false, "Verbose mode")
3650

3751
_ = cobra.MarkFlagRequired(flags, "filter-path")
3852
return
@@ -41,6 +55,7 @@ func NewRootCmd() (c *cobra.Command) {
4155
type responseFilter struct {
4256
urlFilter *filter.URLPathFilter
4357
collects *pkg.Collects
58+
ctx context.Context
4459
}
4560

4661
func (f *responseFilter) filter(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
@@ -51,21 +66,42 @@ func (f *responseFilter) filter(resp *http.Response, ctx *goproxy.ProxyCtx) *htt
5166

5267
req := resp.Request
5368
if f.urlFilter.Filter(req.URL) {
54-
f.collects.Add(req.Clone(context.TODO()))
69+
simpleResp := &pkg.SimpleResponse{StatusCode: resp.StatusCode}
70+
71+
if resp.Body != nil {
72+
buf := new(bytes.Buffer)
73+
io.Copy(buf, resp.Body)
74+
simpleResp.Body = buf.String()
75+
resp.Body = io.NopCloser(buf)
76+
}
77+
78+
f.collects.Add(req.Clone(f.ctx), simpleResp)
5579
}
5680
return resp
5781
}
5882

5983
func (o *option) runE(cmd *cobra.Command, args []string) (err error) {
6084
urlFilter := &filter.URLPathFilter{PathPrefix: o.filterPath}
6185
collects := pkg.NewCollects()
62-
responseFilter := &responseFilter{urlFilter: urlFilter, collects: collects}
86+
responseFilter := &responseFilter{urlFilter: urlFilter, collects: collects, ctx: cmd.Context()}
6387

6488
proxy := goproxy.NewProxyHttpServer()
65-
proxy.Verbose = true
89+
proxy.Verbose = o.verbose
90+
if o.upstreamProxy != "" {
91+
proxy.Tr.Proxy = func(r *http.Request) (*url.URL, error) {
92+
return url.Parse(o.upstreamProxy)
93+
}
94+
proxy.ConnectDial = proxy.NewConnectDialToProxy(o.upstreamProxy)
95+
cmd.Println("Using upstream proxy", o.upstreamProxy)
96+
}
97+
if o.username != "" && o.password != "" {
98+
auth.ProxyBasic(proxy, "my_realm", func(user, pwd string) bool {
99+
return user == o.username && o.password == pwd
100+
})
101+
}
66102
proxy.OnResponse().DoFunc(responseFilter.filter)
67103

68-
exporter := pkg.NewSampleExporter()
104+
exporter := pkg.NewSampleExporter(o.saveResponseBody)
69105
collects.AddEvent(exporter.Add)
70106

71107
srv := &http.Server{

extensions/collector/cmd/collect_test.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package cmd
22

33
import (
4+
"bytes"
5+
"context"
6+
"io"
47
"net/http"
58
"net/url"
69
"testing"
@@ -17,19 +20,26 @@ func TestNewRootCmd(t *testing.T) {
1720
}
1821

1922
func TestResponseFilter(t *testing.T) {
23+
targetURL, err := url.Parse("http://foo.com/api/v1")
24+
assert.NoError(t, err)
25+
2026
resp := &http.Response{
2127
Header: http.Header{
2228
"Content-Type": []string{"application/json; charset=utf-8"},
2329
},
2430
Request: &http.Request{
25-
URL: &url.URL{},
31+
URL: targetURL,
2632
},
33+
Body: io.NopCloser(bytes.NewBuffer([]byte("hello"))),
2734
}
2835
emptyResp := &http.Response{}
2936

3037
filter := &responseFilter{
31-
urlFilter: &filter.URLPathFilter{},
32-
collects: pkg.NewCollects(),
38+
urlFilter: &filter.URLPathFilter{
39+
PathPrefix: []string{"/api/v1"},
40+
},
41+
collects: pkg.NewCollects(),
42+
ctx: context.Background(),
3343
}
3444
filter.filter(emptyResp, nil)
3545
filter.filter(resp, nil)

extensions/collector/pkg/collector.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,46 @@ type Collects struct {
1111
once sync.Once
1212
signal chan string
1313
stopSignal chan struct{}
14-
keys map[string]*http.Request
14+
keys map[string]*RequestAndResponse
1515
requests []*http.Request
1616
events []EventHandle
1717
}
1818

19+
type SimpleResponse struct {
20+
StatusCode int
21+
Body string
22+
}
23+
24+
type RequestAndResponse struct {
25+
Request *http.Request
26+
Response *SimpleResponse
27+
}
28+
1929
// NewCollects creates an instance of Collector
2030
func NewCollects() *Collects {
2131
return &Collects{
2232
once: sync.Once{},
2333
signal: make(chan string, 5),
2434
stopSignal: make(chan struct{}, 1),
25-
keys: make(map[string]*http.Request),
35+
keys: make(map[string]*RequestAndResponse),
2636
}
2737
}
2838

2939
// Add adds a HTTP request
30-
func (c *Collects) Add(req *http.Request) {
40+
func (c *Collects) Add(req *http.Request, resp *SimpleResponse) {
3141
key := fmt.Sprintf("%s-%s", req.Method, req.URL.String())
3242
if _, ok := c.keys[key]; !ok {
33-
c.keys[key] = req
43+
c.keys[key] = &RequestAndResponse{
44+
Request: req,
45+
Response: resp,
46+
}
3447
c.requests = append(c.requests, req)
3548
c.signal <- key
3649
}
3750
}
3851

3952
// EventHandle is the collect event handle
40-
type EventHandle func(r *http.Request)
53+
type EventHandle func(r *RequestAndResponse)
4154

4255
// AddEvent adds new event handle
4356
func (c *Collects) AddEvent(e EventHandle) {
@@ -60,7 +73,6 @@ func (c *Collects) handleEvents() {
6073
case key := <-c.signal:
6174
fmt.Println("receive signal", key)
6275
for _, e := range c.events {
63-
fmt.Println("handle event", key, e)
6476
e(c.keys[key])
6577
}
6678
case <-c.stopSignal:

extensions/collector/pkg/collector_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ func TestCollector(t *testing.T) {
2121
for _, tt := range tests {
2222
t.Run(tt.name, func(t *testing.T) {
2323
collects := pkg.NewCollects()
24-
collects.AddEvent(func(r *http.Request) {
24+
collects.AddEvent(func(reqAndResp *pkg.RequestAndResponse) {
25+
r := reqAndResp.Request
2526
assert.Equal(t, tt.Request, r)
2627
})
2728
for i := 0; i < 10; i++ {
28-
collects.Add(tt.Request)
29+
collects.Add(tt.Request, nil)
2930
}
3031
collects.Stop()
3132
})

extensions/collector/pkg/exporter.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package pkg
33
import (
44
"fmt"
55
"io"
6-
"net/http"
76
"strings"
87

98
atestpkg "github.com/linuxsuren/api-testing/pkg/testing"
@@ -12,20 +11,23 @@ import (
1211

1312
// SampleExporter is a sample exporter
1413
type SampleExporter struct {
15-
TestSuite atestpkg.TestSuite
14+
TestSuite atestpkg.TestSuite
15+
saveResponseBody bool
1616
}
1717

1818
// NewSampleExporter creates a new exporter
19-
func NewSampleExporter() *SampleExporter {
19+
func NewSampleExporter(saveResponseBody bool) *SampleExporter {
2020
return &SampleExporter{
2121
TestSuite: atestpkg.TestSuite{
2222
Name: "sample",
2323
},
24+
saveResponseBody: saveResponseBody,
2425
}
2526
}
2627

2728
// Add adds a request to the exporter
28-
func (e *SampleExporter) Add(r *http.Request) {
29+
func (e *SampleExporter) Add(reqAndResp *RequestAndResponse) {
30+
r, resp := reqAndResp.Request, reqAndResp.Response
2931

3032
fmt.Println("receive", r.URL.Path)
3133
req := atestpkg.Request{
@@ -42,9 +44,13 @@ func (e *SampleExporter) Add(r *http.Request) {
4244

4345
testCase := atestpkg.TestCase{
4446
Request: req,
45-
Expect: atestpkg.Response{
46-
StatusCode: http.StatusOK,
47-
},
47+
}
48+
49+
if resp != nil {
50+
testCase.Expect.StatusCode = resp.StatusCode
51+
if e.saveResponseBody && resp.Body != "" {
52+
testCase.Expect.Body = resp.Body
53+
}
4854
}
4955

5056
specs := strings.Split(r.URL.Path, "/")

extensions/collector/pkg/exporter_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,21 @@ import (
1212
)
1313

1414
func TestSampleExporter(t *testing.T) {
15-
exporter := pkg.NewSampleExporter()
15+
exporter := pkg.NewSampleExporter(true)
1616
assert.Equal(t, "sample", exporter.TestSuite.Name)
1717

1818
request, err := newRequest()
1919
assert.NoError(t, err)
20-
exporter.Add(request)
20+
exporter.Add(&pkg.RequestAndResponse{Request: request})
2121

2222
request, err = newRequest()
23-
exporter.Add(request)
23+
exporter.Add(&pkg.RequestAndResponse{
24+
Request: request,
25+
Response: &pkg.SimpleResponse{
26+
Body: "hello",
27+
StatusCode: http.StatusOK,
28+
},
29+
})
2430

2531
var result string
2632
result, err = exporter.Export()
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package filter
22

33
import (
4-
"fmt"
54
"net/url"
65
"strings"
76
)
@@ -13,11 +12,15 @@ type URLFilter interface {
1312

1413
// URLPathFilter filters the URL with path
1514
type URLPathFilter struct {
16-
PathPrefix string
15+
PathPrefix []string
1716
}
1817

1918
// Filter implements the URLFilter
2019
func (f *URLPathFilter) Filter(targetURL *url.URL) bool {
21-
fmt.Println(targetURL.Path, f.PathPrefix)
22-
return strings.HasPrefix(targetURL.Path, f.PathPrefix)
20+
for _, prefix := range f.PathPrefix {
21+
if strings.HasPrefix(targetURL.Path, prefix) {
22+
return true
23+
}
24+
}
25+
return false
2326
}

extensions/collector/pkg/filter/url_filter_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
)
1010

1111
func TestURLPathFilter(t *testing.T) {
12-
urlFilter := &filter.URLPathFilter{PathPrefix: "/api"}
12+
urlFilter := &filter.URLPathFilter{PathPrefix: []string{"/api/v1", "/api/v2"}}
1313
assert.True(t, urlFilter.Filter(&url.URL{Path: "/api/v1"}))
14+
assert.True(t, urlFilter.Filter(&url.URL{Path: "/api/v2"}))
15+
assert.False(t, urlFilter.Filter(&url.URL{Path: "/api/v3"}))
1416
}

extensions/collector/pkg/testdata/sample_suite.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ items:
1010
Authorization: Bearer token
1111
Content-Type: application/json
1212
body: hello
13-
expect:
14-
statusCode: 200
1513
- name: v1-1
1614
request:
1715
api: http://foo/api/v1
@@ -22,3 +20,4 @@ items:
2220
body: hello
2321
expect:
2422
statusCode: 200
23+
body: hello

0 commit comments

Comments
 (0)