use Transport to intercept Request and Response
This commit is contained in:
parent
ac3a7e0300
commit
bfa5e78a26
12
README.md
12
README.md
|
@ -1,17 +1,17 @@
|
||||||
|
|
||||||
**GoCapture** is a reverse proxy that captures the network traffic and shows it in a dashboard
|
**Capture** is a reverse proxy that captures the network traffic and shows it in a dashboard
|
||||||
|
|
||||||
|
|
||||||
## Building / Running
|
## Building / Running
|
||||||
|
|
||||||
git clone https://github.com/ofabricio/gocapture.git
|
git clone https://github.com/ofabricio/capture.git
|
||||||
cd gocapture
|
cd capture
|
||||||
go build
|
go build
|
||||||
./gocapture -url=https://example.com/api -port=9000 -dashboard=apple -max-captures=16
|
./capture -url=https://example.com/api -port=9000 -dashboard=apple -max-captures=16
|
||||||
|
|
||||||
### Binaries / Executables
|
### Binaries / Executables
|
||||||
|
|
||||||
For ready-to-use executables (no need to build it yourself) for *Windows* and *Linux*, see [Releases](https://github.com/ofabricio/gocapture/releases) page
|
For ready-to-use executables (no need to build it yourself) for *Windows* and *Linux*, see [Releases](https://github.com/ofabricio/capture/releases) page
|
||||||
|
|
||||||
### Configurations
|
### Configurations
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ For ready-to-use executables (no need to build it yourself) for *Windows* and *L
|
||||||
If you set your base url as `http://example.com/api`, now `http://localhost:9000` points to that
|
If you set your base url as `http://example.com/api`, now `http://localhost:9000` points to that
|
||||||
address. Hence, calling `http://localhost:9000/users/1` is like calling `http://example.com/api/users/1`
|
address. Hence, calling `http://localhost:9000/users/1` is like calling `http://example.com/api/users/1`
|
||||||
|
|
||||||
*GoCapture* saves all requests and responses so that you can see them in the dashboard
|
*Capture* saves all requests and responses so that you can see them in the dashboard
|
||||||
|
|
||||||
|
|
||||||
## Dashboard
|
## Dashboard
|
||||||
|
|
60
capture.go
60
capture.go
|
@ -1,60 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/gzip"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Capture map[string]interface{}
|
|
||||||
|
|
||||||
func (capture Capture) Write(request *http.Request, reqBody io.Reader, response *ResponseWrapper) {
|
|
||||||
capture["url"] = request.URL.Path
|
|
||||||
capture["method"] = request.Method
|
|
||||||
capture["request"] = createRequestMap(request, reqBody)
|
|
||||||
capture["response"] = createResponseMap(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createRequestMap(request *http.Request, reqBody io.Reader) map[string]interface{} {
|
|
||||||
return createHeaderAndBodyMap(request.Header, reqBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createResponseMap(response *ResponseWrapper) map[string]interface{} {
|
|
||||||
responseMap := createHeaderAndBodyMap(response.Header(), response.Body)
|
|
||||||
responseMap["status"] = response.Status
|
|
||||||
return responseMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func createHeaderAndBodyMap(headers http.Header, body io.Reader) map[string]interface{} {
|
|
||||||
obj := make(map[string]interface{})
|
|
||||||
obj["headers"] = getHeaders(headers)
|
|
||||||
obj["body"] = getBody(headers, body)
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHeaders(headers http.Header) map[string]string {
|
|
||||||
flatHeaders := make(map[string]string)
|
|
||||||
for key, values := range headers {
|
|
||||||
flatHeaders[key] = strings.Join(values, "; ")
|
|
||||||
}
|
|
||||||
return flatHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBody(headers http.Header, body io.Reader) map[string]interface{} {
|
|
||||||
body = unzip(headers, body)
|
|
||||||
bbody, _ := ioutil.ReadAll(body)
|
|
||||||
bodyUnmarshal := make(map[string]interface{})
|
|
||||||
json.Unmarshal(bbody, &bodyUnmarshal)
|
|
||||||
return bodyUnmarshal
|
|
||||||
}
|
|
||||||
|
|
||||||
func unzip(headers http.Header, body io.Reader) io.Reader {
|
|
||||||
if headers.Get("Content-Encoding") == "gzip" {
|
|
||||||
uncompressed, _ := gzip.NewReader(body)
|
|
||||||
return uncompressed
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
142
dashboard.go
142
dashboard.go
File diff suppressed because one or more lines are too long
52
main.go
52
main.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -9,15 +10,23 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Capture map[string]interface{}
|
||||||
|
|
||||||
var captures []Capture
|
var captures []Capture
|
||||||
var maxCaptures int
|
var maxCaptures int
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
http.RoundTripper
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
args := parseArgs()
|
args := parseArgs()
|
||||||
maxCaptures = args.maxCaptures
|
maxCaptures = args.maxCaptures
|
||||||
|
|
||||||
URL, _ := url.Parse(args.url)
|
URL, _ := url.Parse(args.url)
|
||||||
proxy := httputil.NewSingleHostReverseProxy(URL)
|
proxy := httputil.NewSingleHostReverseProxy(URL)
|
||||||
|
proxy.Transport = Transport{http.DefaultTransport}
|
||||||
|
|
||||||
http.Handle("/", getProxyHandler(proxy))
|
http.Handle("/", getProxyHandler(proxy))
|
||||||
http.Handle("/socket.io/", getSocketHandler())
|
http.Handle("/socket.io/", getSocketHandler())
|
||||||
http.Handle("/"+args.dashboard+"/", getDashboardHandler())
|
http.Handle("/"+args.dashboard+"/", getDashboardHandler())
|
||||||
|
@ -27,21 +36,42 @@ func main() {
|
||||||
func getProxyHandler(handler http.Handler) http.Handler {
|
func getProxyHandler(handler http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||||
request.Host = request.URL.Host
|
request.Host = request.URL.Host
|
||||||
|
handler.ServeHTTP(response, request)
|
||||||
var reqBody bytes.Buffer
|
|
||||||
request.Body = ioutil.NopCloser(io.TeeReader(request.Body, &reqBody))
|
|
||||||
|
|
||||||
responseWrapper := NewResponseWrapper(response)
|
|
||||||
handler.ServeHTTP(responseWrapper, request)
|
|
||||||
|
|
||||||
saveRequestAndResponse(request, &reqBody, responseWrapper)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveRequestAndResponse(request *http.Request, reqBody io.Reader, response *ResponseWrapper) {
|
func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
capture := Capture{}
|
reqDump, _ := httputil.DumpRequest(req, true)
|
||||||
capture.Write(request, reqBody, response)
|
|
||||||
|
|
||||||
|
res, e := t.RoundTripper.RoundTrip(req)
|
||||||
|
|
||||||
|
resDump, _ := DumpResponse(res)
|
||||||
|
|
||||||
|
capture := Capture{
|
||||||
|
"url": req.URL.Path,
|
||||||
|
"method": req.Method,
|
||||||
|
"status": res.StatusCode,
|
||||||
|
"request": string(reqDump),
|
||||||
|
"response": string(resDump),
|
||||||
|
}
|
||||||
|
|
||||||
|
save(capture)
|
||||||
|
|
||||||
|
return res, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func DumpResponse(res *http.Response) ([]byte, error) {
|
||||||
|
var originalBody bytes.Buffer
|
||||||
|
res.Body = ioutil.NopCloser(io.TeeReader(res.Body, &originalBody))
|
||||||
|
if res.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
res.Body, _ = gzip.NewReader(res.Body)
|
||||||
|
}
|
||||||
|
resDump, e := httputil.DumpResponse(res, true)
|
||||||
|
res.Body = ioutil.NopCloser(&originalBody)
|
||||||
|
return resDump, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(capture Capture) {
|
||||||
captures = append([]Capture{capture}, captures...)
|
captures = append([]Capture{capture}, captures...)
|
||||||
if len(captures) > maxCaptures {
|
if len(captures) > maxCaptures {
|
||||||
captures = captures[:len(captures)-1]
|
captures = captures[:len(captures)-1]
|
||||||
|
|
31
response.go
31
response.go
|
@ -1,31 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ResponseWrapper struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
Status int
|
|
||||||
Body io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewResponseWrapper(response http.ResponseWriter) *ResponseWrapper {
|
|
||||||
return &ResponseWrapper{response, http.StatusInternalServerError, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response *ResponseWrapper) WriteHeader(code int) {
|
|
||||||
response.Status = code
|
|
||||||
response.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response *ResponseWrapper) Write(body []byte) (int, error) {
|
|
||||||
response.Body = bytes.NewBuffer(body)
|
|
||||||
return response.ResponseWriter.Write(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (response *ResponseWrapper) Header() http.Header {
|
|
||||||
return response.ResponseWriter.Header()
|
|
||||||
}
|
|
Loading…
Reference in a new issue