initial commit
This commit is contained in:
commit
ac3a7e0300
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
debug
|
||||||
|
debug.test
|
||||||
|
*.exe
|
44
README.md
Normal file
44
README.md
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
**GoCapture** is a reverse proxy that captures the network traffic and shows it in a dashboard
|
||||||
|
|
||||||
|
|
||||||
|
## Building / Running
|
||||||
|
|
||||||
|
git clone https://github.com/ofabricio/gocapture.git
|
||||||
|
cd gocapture
|
||||||
|
go build
|
||||||
|
./gocapture -url=https://example.com/api -port=9000 -dashboard=apple -max-captures=16
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Configurations
|
||||||
|
|
||||||
|
| param | description |
|
||||||
|
|-----------------|-------------|
|
||||||
|
| `-url` | **Required.** Set the base url you want to capture |
|
||||||
|
| `-port` | Set the port you want to capture. Default: *9000* |
|
||||||
|
| `-dashboard` | Set the dashboard name. Default: *dashboard* |
|
||||||
|
| `-max-captures` | Set the max number of captures. Default: *16* |
|
||||||
|
| `-h` | Show help |
|
||||||
|
|
||||||
|
|
||||||
|
## Using
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
*GoCapture* saves all requests and responses so that you can see them in the dashboard
|
||||||
|
|
||||||
|
|
||||||
|
## Dashboard
|
||||||
|
|
||||||
|
To access the dashboard go to `http://localhost:9000/dashboard`
|
||||||
|
|
||||||
|
The path `/dashboard/**` is reserved, that means if your api has a path like that it will be ignored
|
||||||
|
in favor of the dashboard. However, you can change the dashboard's name with `-dashboard`
|
||||||
|
|
||||||
|
##### Preview
|
||||||
|
|
||||||
|
![dashboard](https://i.imgur.com/13nzb48.png)
|
27
args.go
Normal file
27
args.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Args struct {
|
||||||
|
url string
|
||||||
|
port string
|
||||||
|
dashboard string
|
||||||
|
maxCaptures int
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseArgs() Args {
|
||||||
|
proxyURL := flag.String("url", "https://jsonplaceholder.typicode.com", "Required. Set the base url you want to capture")
|
||||||
|
proxyPort := flag.Int("port", 9000, "Set the port you want to capture")
|
||||||
|
maxCaptures := flag.Int("max-captures", 16, "Set the max number of captures")
|
||||||
|
dashboard := flag.String("dashboard", "dashboard", "Set the dashboard name")
|
||||||
|
flag.Parse()
|
||||||
|
return Args{
|
||||||
|
url: *proxyURL,
|
||||||
|
port: strconv.Itoa(*proxyPort),
|
||||||
|
dashboard: *dashboard,
|
||||||
|
maxCaptures: *maxCaptures,
|
||||||
|
}
|
||||||
|
}
|
60
capture.go
Normal file
60
capture.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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
|
||||||
|
}
|
16
dashboard.go
Normal file
16
dashboard.go
Normal file
File diff suppressed because one or more lines are too long
50
main.go
Normal file
50
main.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
var captures []Capture
|
||||||
|
var maxCaptures int
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := parseArgs()
|
||||||
|
maxCaptures = args.maxCaptures
|
||||||
|
|
||||||
|
URL, _ := url.Parse(args.url)
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(URL)
|
||||||
|
http.Handle("/", getProxyHandler(proxy))
|
||||||
|
http.Handle("/socket.io/", getSocketHandler())
|
||||||
|
http.Handle("/"+args.dashboard+"/", getDashboardHandler())
|
||||||
|
http.ListenAndServe(":"+args.port, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProxyHandler(handler http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
||||||
|
request.Host = request.URL.Host
|
||||||
|
|
||||||
|
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) {
|
||||||
|
capture := Capture{}
|
||||||
|
capture.Write(request, reqBody, response)
|
||||||
|
|
||||||
|
captures = append([]Capture{capture}, captures...)
|
||||||
|
if len(captures) > maxCaptures {
|
||||||
|
captures = captures[:len(captures)-1]
|
||||||
|
}
|
||||||
|
emit(captures)
|
||||||
|
}
|
31
response.go
Normal file
31
response.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
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()
|
||||||
|
}
|
39
socket.go
Normal file
39
socket.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/googollee/go-socket.io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var socket socketio.Socket
|
||||||
|
|
||||||
|
func getSocketHandler() http.Handler {
|
||||||
|
server, err := socketio.NewServer(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.On("connection", func(so socketio.Socket) {
|
||||||
|
socket = so
|
||||||
|
log.Println("dashboard is connected")
|
||||||
|
|
||||||
|
so.On("disconnection", func() {
|
||||||
|
log.Println("dashboard is disconnected")
|
||||||
|
})
|
||||||
|
|
||||||
|
emit(captures)
|
||||||
|
})
|
||||||
|
server.On("error", func(so socketio.Socket, err error) {
|
||||||
|
log.Println("socket error:", err)
|
||||||
|
})
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func emit(data interface{}) {
|
||||||
|
if socket == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
socket.Emit("captures", data)
|
||||||
|
}
|
Loading…
Reference in a new issue