exchange socketio for SSE
This commit is contained in:
parent
f7de340a06
commit
1706954423
33
capture.go
33
capture.go
|
@ -9,17 +9,12 @@ import (
|
||||||
var captureID int
|
var captureID int
|
||||||
var captures CaptureList
|
var captures CaptureList
|
||||||
|
|
||||||
type CaptureRepository interface {
|
|
||||||
Insert(capture Capture)
|
|
||||||
RemoveAll()
|
|
||||||
Find(captureID string) *Capture
|
|
||||||
FindAll() []Capture
|
|
||||||
}
|
|
||||||
|
|
||||||
type CaptureList struct {
|
type CaptureList struct {
|
||||||
items []Capture
|
items []Capture
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
maxItems int
|
maxItems int
|
||||||
|
// signals any change in "items"
|
||||||
|
Updated chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Capture struct {
|
type Capture struct {
|
||||||
|
@ -50,9 +45,10 @@ func (c *Capture) Metadata() CaptureMetadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCapturesRepository(maxItems int) CaptureRepository {
|
func NewCaptureList(maxItems int) *CaptureList {
|
||||||
return &CaptureList{
|
return &CaptureList{
|
||||||
maxItems: maxItems,
|
maxItems: maxItems,
|
||||||
|
Updated: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +60,7 @@ func (c *CaptureList) Insert(capture Capture) {
|
||||||
if len(c.items) > c.maxItems {
|
if len(c.items) > c.maxItems {
|
||||||
c.items = c.items[1:]
|
c.items = c.items[1:]
|
||||||
}
|
}
|
||||||
|
c.signalsItemsChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CaptureList) Find(captureID string) *Capture {
|
func (c *CaptureList) Find(captureID string) *Capture {
|
||||||
|
@ -82,13 +79,31 @@ func (c *CaptureList) RemoveAll() {
|
||||||
c.mux.Lock()
|
c.mux.Lock()
|
||||||
defer c.mux.Unlock()
|
defer c.mux.Unlock()
|
||||||
c.items = nil
|
c.items = nil
|
||||||
|
c.signalsItemsChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CaptureList) FindAll() []Capture {
|
func (c *CaptureList) Items() []Capture {
|
||||||
return c.items
|
return c.items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CaptureList) ItemsAsMetadata() []CaptureMetadata {
|
||||||
|
c.mux.Lock()
|
||||||
|
defer c.mux.Unlock()
|
||||||
|
metadatas := make([]CaptureMetadata, len(c.items))
|
||||||
|
for i, capture := range c.items {
|
||||||
|
metadatas[i] = capture.Metadata()
|
||||||
|
}
|
||||||
|
return metadatas
|
||||||
|
}
|
||||||
|
|
||||||
func newID() int {
|
func newID() int {
|
||||||
captureID++
|
captureID++
|
||||||
return captureID
|
return captureID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CaptureList) signalsItemsChange() {
|
||||||
|
select {
|
||||||
|
case c.Updated <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ func ReadConfig() Config {
|
||||||
maxCaptures := flag.Int("max-captures", 16, "Set the max number of captures to show in the dashboard")
|
maxCaptures := flag.Int("max-captures", 16, "Set the max number of captures to show in the dashboard")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
dashboardConnPath := "/socket.io/"
|
|
||||||
dashboardPath := fmt.Sprintf("/%s/", *dashboard)
|
dashboardPath := fmt.Sprintf("/%s/", *dashboard)
|
||||||
|
dashboardConnPath := fmt.Sprintf("/%s/conn/", *dashboard)
|
||||||
dashboardClearPath := fmt.Sprintf("/%s/clear/", *dashboard)
|
dashboardClearPath := fmt.Sprintf("/%s/clear/", *dashboard)
|
||||||
dashboardItemInfoPath := fmt.Sprintf("/%s/items/", *dashboard)
|
dashboardItemInfoPath := fmt.Sprintf("/%s/items/", *dashboard)
|
||||||
|
|
||||||
|
|
25
dashboard.go
25
dashboard.go
|
@ -7,7 +7,6 @@ const dashboardHTML = `
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.2/angular.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.2/angular.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.slim.js"></script>
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Inconsolata:400,700" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css?family=Inconsolata:400,700" rel="stylesheet">
|
||||||
<title>Dashboard</title>
|
<title>Dashboard</title>
|
||||||
<style>
|
<style>
|
||||||
|
@ -210,7 +209,7 @@ const dashboardHTML = `
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="welcome" ng-show="items.length == 0">
|
<div class="welcome" ng-show="items.length == 0">
|
||||||
Waiting for requests on http://localhost:{{config.proxyPort}}/
|
Waiting for requests on http://localhost:<<.ProxyPort>>/
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -222,7 +221,7 @@ const dashboardHTML = `
|
||||||
$scope.show = item => {
|
$scope.show = item => {
|
||||||
$scope.path = item.path;
|
$scope.path = item.path;
|
||||||
$scope.selectedId = item.id;
|
$scope.selectedId = item.id;
|
||||||
let path = $scope.config.dashboardItemInfoPath + item.id;
|
let path = <<.DashboardItemInfoPath>> + item.id;
|
||||||
$http.get(path).then(r => {
|
$http.get(path).then(r => {
|
||||||
$scope.request = r.data.request;
|
$scope.request = r.data.request;
|
||||||
$scope.response = r.data.response;
|
$scope.response = r.data.response;
|
||||||
|
@ -240,7 +239,7 @@ const dashboardHTML = `
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.clearDashboard = () => {
|
$scope.clearDashboard = () => {
|
||||||
$http.get($scope.config.dashboardClearPath).then(clearRequestAndResponse);
|
$http.get(<<.DashboardClearPath>>).then(clearRequestAndResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearRequestAndResponse() {
|
function clearRequestAndResponse() {
|
||||||
|
@ -276,18 +275,14 @@ const dashboardHTML = `
|
||||||
$scope[key] = data.replace(body, prettyBody);
|
$scope[key] = data.replace(body, prettyBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
let socket = io();
|
const evt = new EventSource(<<.DashboardConnPath>>);
|
||||||
socket.on('connect', () => {
|
evt.addEventListener('connected', e => {
|
||||||
clearRequestAndResponse();
|
clearRequestAndResponse();
|
||||||
socket.off('config');
|
$scope.$apply();
|
||||||
socket.off('captures');
|
});
|
||||||
socket.on('config', args => {
|
evt.addEventListener('captures', e => {
|
||||||
$scope.config = args;
|
$scope.items = JSON.parse(e.data);
|
||||||
});
|
$scope.$apply();
|
||||||
socket.on('captures', captures => {
|
|
||||||
$scope.items = captures;
|
|
||||||
$scope.$apply();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
7
go.mod
7
go.mod
|
@ -1,8 +1,3 @@
|
||||||
module github.com/ofabricio/capture
|
module github.com/ofabricio/capture
|
||||||
|
|
||||||
require (
|
require github.com/ofabricio/curl v0.1.0
|
||||||
github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb // indirect
|
|
||||||
github.com/googollee/go-socket.io v0.0.0-20181101151912-c8aeb1ed9b49
|
|
||||||
github.com/gorilla/websocket v1.4.0 // indirect
|
|
||||||
github.com/ofabricio/curl v0.1.0
|
|
||||||
)
|
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1,8 +1,2 @@
|
||||||
github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb h1:n22Aukg/TjoypWc37dbKIpCsz0VMFPD36HQk1WKvg3A=
|
|
||||||
github.com/googollee/go-engine.io v0.0.0-20180829091931-e2f255711dcb/go.mod h1:MBpz1MS3P4HtRcBpQU4HcjvWXZ9q+JWacMEh2/BFYbg=
|
|
||||||
github.com/googollee/go-socket.io v0.0.0-20181101151912-c8aeb1ed9b49 h1:vKXGRzlhWE9TUVhLqAOcgQbfYvReAnsvQQIcnvWMfcg=
|
|
||||||
github.com/googollee/go-socket.io v0.0.0-20181101151912-c8aeb1ed9b49/go.mod h1:ftBGBMhSYToR5oV4ImIPKvAIsNaTkLC+tTvoNafqxlQ=
|
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
|
||||||
github.com/ofabricio/curl v0.1.0 h1:ntXuBULZLQmCdAMxZNXzse069DbAKb/Flxe/2uuZuNk=
|
github.com/ofabricio/curl v0.1.0 h1:ntXuBULZLQmCdAMxZNXzse069DbAKb/Flxe/2uuZuNk=
|
||||||
github.com/ofabricio/curl v0.1.0/go.mod h1:RtLkZIOgxjm+l0jdj04lrETzu8u5SmPPdLyGAuC4ukg=
|
github.com/ofabricio/curl v0.1.0/go.mod h1:RtLkZIOgxjm+l0jdj04lrETzu8u5SmPPdLyGAuC4ukg=
|
||||||
|
|
89
main.go
89
main.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -14,12 +15,9 @@ import (
|
||||||
"plugin"
|
"plugin"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/googollee/go-socket.io"
|
|
||||||
"github.com/ofabricio/curl"
|
"github.com/ofabricio/curl"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dashboardSocket socketio.Socket
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := ReadConfig()
|
config := ReadConfig()
|
||||||
startCapture(config)
|
startCapture(config)
|
||||||
|
@ -27,13 +25,13 @@ func main() {
|
||||||
|
|
||||||
func startCapture(config Config) {
|
func startCapture(config Config) {
|
||||||
|
|
||||||
repo := NewCapturesRepository(config.MaxCaptures)
|
list := NewCaptureList(config.MaxCaptures)
|
||||||
|
|
||||||
http.Handle("/", NewPlugin(NewRecorder(repo, NewProxyHandler(config.TargetURL))))
|
http.Handle("/", NewPlugin(NewRecorder(list, NewProxyHandler(config.TargetURL))))
|
||||||
http.Handle(config.DashboardPath, NewDashboardHtmlHandler())
|
http.Handle(config.DashboardPath, NewDashboardHtmlHandler(config))
|
||||||
http.Handle(config.DashboardClearPath, NewDashboardClearHandler(repo))
|
http.Handle(config.DashboardConnPath, NewDashboardConnHandler(list))
|
||||||
http.Handle(config.DashboardItemInfoPath, NewDashboardItemInfoHandler(repo))
|
http.Handle(config.DashboardClearPath, NewDashboardClearHandler(list))
|
||||||
http.Handle(config.DashboardConnPath, NewDashboardSocketHandler(repo, config))
|
http.Handle(config.DashboardItemInfoPath, NewDashboardItemInfoHandler(list))
|
||||||
|
|
||||||
captureHost := fmt.Sprintf("http://localhost:%s", config.ProxyPort)
|
captureHost := fmt.Sprintf("http://localhost:%s", config.ProxyPort)
|
||||||
|
|
||||||
|
@ -43,41 +41,58 @@ func startCapture(config Config) {
|
||||||
fmt.Println(http.ListenAndServe(":"+config.ProxyPort, nil))
|
fmt.Println(http.ListenAndServe(":"+config.ProxyPort, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashboardSocketHandler(repo CaptureRepository, config Config) http.Handler {
|
func NewDashboardConnHandler(list *CaptureList) http.Handler {
|
||||||
server, err := socketio.NewServer(nil)
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
if err != nil {
|
if _, ok := rw.(http.Flusher); !ok {
|
||||||
fmt.Printf("socket server error: %v\n", err)
|
fmt.Printf("streaming not supported at %s\n", req.URL)
|
||||||
}
|
http.Error(rw, "streaming not supported", http.StatusInternalServerError)
|
||||||
server.On("connection", func(so socketio.Socket) {
|
return
|
||||||
dashboardSocket = so
|
}
|
||||||
dashboardSocket.Emit("config", config)
|
rw.Header().Set("Content-Type", "text/event-stream")
|
||||||
emitToDashboard(repo.FindAll())
|
rw.Header().Set("Cache-Control", "no-cache")
|
||||||
|
|
||||||
|
fmt.Fprintf(rw, "event: connected\ndata: %s\n\n", "clear")
|
||||||
|
rw.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
for {
|
||||||
|
jsn, _ := json.Marshal(list.ItemsAsMetadata())
|
||||||
|
fmt.Fprintf(rw, "event: captures\ndata: %s\n\n", jsn)
|
||||||
|
rw.(http.Flusher).Flush()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-list.Updated:
|
||||||
|
case <-req.Context().Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
server.On("error", func(so socketio.Socket, err error) {
|
|
||||||
fmt.Printf("socket error: %v\n", err)
|
|
||||||
})
|
|
||||||
return server
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashboardClearHandler(repo CaptureRepository) http.Handler {
|
func NewDashboardClearHandler(list *CaptureList) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
repo.RemoveAll()
|
list.RemoveAll()
|
||||||
emitToDashboard(nil)
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashboardHtmlHandler() http.Handler {
|
func NewDashboardHtmlHandler(config Config) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
rw.Header().Add("Content-Type", "text/html")
|
rw.Header().Add("Content-Type", "text/html")
|
||||||
fmt.Fprint(rw, dashboardHTML)
|
t, err := template.New("dashboard template").Delims("<<", ">>").Parse(dashboardHTML)
|
||||||
|
if err != nil {
|
||||||
|
msg := fmt.Sprintf("could not parse dashboard html template: %v", err)
|
||||||
|
fmt.Println(msg)
|
||||||
|
http.Error(rw, msg, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Execute(rw, config)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDashboardItemInfoHandler(repo CaptureRepository) http.Handler {
|
func NewDashboardItemInfoHandler(list *CaptureList) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
id := req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]
|
id := req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]
|
||||||
capture := repo.Find(id)
|
capture := list.Find(id)
|
||||||
if capture == nil {
|
if capture == nil {
|
||||||
http.Error(rw, "Item Not Found", http.StatusNotFound)
|
http.Error(rw, "Item Not Found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -108,7 +123,7 @@ func NewPlugin(next http.Handler) http.Handler {
|
||||||
return pluginFn(next)
|
return pluginFn(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecorder(repo CaptureRepository, next http.Handler) http.Handler {
|
func NewRecorder(list *CaptureList, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
// save req body for later
|
// save req body for later
|
||||||
|
@ -129,8 +144,7 @@ func NewRecorder(repo CaptureRepository, next http.Handler) http.Handler {
|
||||||
// record req and res
|
// record req and res
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
|
req.Body = ioutil.NopCloser(bytes.NewReader(reqBody))
|
||||||
res := rec.Result()
|
res := rec.Result()
|
||||||
repo.Insert(Capture{Req: req, Res: res})
|
list.Insert(Capture{Req: req, Res: res})
|
||||||
emitToDashboard(repo.FindAll())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,14 +209,3 @@ func drain(b io.ReadCloser) (io.ReadCloser, []byte) {
|
||||||
b.Close()
|
b.Close()
|
||||||
return ioutil.NopCloser(bytes.NewReader(all)), all
|
return ioutil.NopCloser(bytes.NewReader(all)), all
|
||||||
}
|
}
|
||||||
|
|
||||||
func emitToDashboard(captures []Capture) {
|
|
||||||
if dashboardSocket == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
metadatas := make([]CaptureMetadata, len(captures))
|
|
||||||
for i, capture := range captures {
|
|
||||||
metadatas[i] = capture.Metadata()
|
|
||||||
}
|
|
||||||
dashboardSocket.Emit("captures", metadatas)
|
|
||||||
}
|
|
||||||
|
|
|
@ -181,8 +181,8 @@ func TestCaptureIDConcurrence(t *testing.T) {
|
||||||
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
||||||
rw.WriteHeader(http.StatusOK)
|
rw.WriteHeader(http.StatusOK)
|
||||||
}))
|
}))
|
||||||
repo := NewCapturesRepository(interactions)
|
list := NewCaptureList(interactions)
|
||||||
capture := httptest.NewServer(NewRecorder(repo, NewProxyHandler(service.URL)))
|
capture := httptest.NewServer(NewRecorder(list, NewProxyHandler(service.URL)))
|
||||||
defer service.Close()
|
defer service.Close()
|
||||||
defer capture.Close()
|
defer capture.Close()
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ func TestCaptureIDConcurrence(t *testing.T) {
|
||||||
// then
|
// then
|
||||||
|
|
||||||
// Tests if captures IDs are sequential
|
// Tests if captures IDs are sequential
|
||||||
captures := repo.FindAll()
|
captures := list.Items()
|
||||||
if len(captures) == 0 {
|
if len(captures) == 0 {
|
||||||
t.Fatalf("No captures found")
|
t.Fatalf("No captures found")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue