exchange socketio for SSE

This commit is contained in:
Fabricio 2018-11-25 15:10:10 -02:00
parent f7de340a06
commit 1706954423
7 changed files with 85 additions and 83 deletions

View file

@ -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:
}
}

View file

@ -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)

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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)
}

View file

@ -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")
} }