From 894cbe76a11845af953f03c8c49332c7bef69101 Mon Sep 17 00:00:00 2001 From: evilsocket Date: Thu, 1 Mar 2018 18:06:42 +0100 Subject: [PATCH 1/3] refact: refactored api.rest to avoid using global variables --- modules/api_rest.go | 24 +++++++++++----- modules/api_rest_controller.go | 51 +++++++++++++++------------------- 2 files changed, 40 insertions(+), 35 deletions(-) diff --git a/modules/api_rest.go b/modules/api_rest.go index 25fc3398..62f3eabf 100644 --- a/modules/api_rest.go +++ b/modules/api_rest.go @@ -14,15 +14,19 @@ import ( type RestAPI struct { session.SessionModule - server *http.Server - certFile string - keyFile string + server *http.Server + username string + password string + certFile string + keyFile string + useWebsocket bool } func NewRestAPI(s *session.Session) *RestAPI { api := &RestAPI{ SessionModule: session.NewSessionModule("api.rest", s), server: &http.Server{}, + useWebsocket: false, } api.AddParam(session.NewStringParameter("api.rest.address", @@ -54,6 +58,10 @@ func NewRestAPI(s *session.Session) *RestAPI { "", "API TLS key")) + api.AddParam(session.NewBoolParameter("api.rest.websocket", + "false", + "If true the /api/events route will be available as a websocket endpoint instead of HTTPS.")) + api.AddHandler(session.NewModuleHandler("api.rest on", "", "Start REST API server.", func(args []string) error { @@ -106,9 +114,11 @@ func (api *RestAPI) Configure() error { return err } else if api.keyFile, err = core.ExpandPath(api.keyFile); err != nil { return err - } else if err, ApiUsername = api.StringParam("api.rest.username"); err != nil { + } else if err, api.username = api.StringParam("api.rest.username"); err != nil { return err - } else if err, ApiPassword = api.StringParam("api.rest.password"); err != nil { + } else if err, api.password = api.StringParam("api.rest.password"); err != nil { + return err + } else if err, api.useWebsocket = api.BoolParam("api.rest.websocket"); err != nil { return err } else if core.Exists(api.certFile) == false || core.Exists(api.keyFile) == false { log.Info("Generating TLS key to %s", api.keyFile) @@ -125,8 +135,8 @@ func (api *RestAPI) Configure() error { router := http.NewServeMux() - router.HandleFunc("/api/session", SessionRoute) - router.HandleFunc("/api/events", EventsRoute) + router.HandleFunc("/api/session", api.sessionRoute) + router.HandleFunc("/api/events", api.eventsRoute) api.server.Handler = router diff --git a/modules/api_rest_controller.go b/modules/api_rest_controller.go index 05457ae4..717e1f74 100644 --- a/modules/api_rest_controller.go +++ b/modules/api_rest_controller.go @@ -9,11 +9,6 @@ import ( "github.com/bettercap/bettercap/session" ) -var ( - ApiUsername = "" - ApiPassword = "" -) - type CommandRequest struct { Command string `json:"cmd"` } @@ -23,17 +18,6 @@ type APIResponse struct { Message string `json:"msg"` } -func checkAuth(r *http.Request) bool { - user, pass, _ := r.BasicAuth() - // timing attack my ass - if subtle.ConstantTimeCompare([]byte(user), []byte(ApiUsername)) != 1 { - return false - } else if subtle.ConstantTimeCompare([]byte(pass), []byte(ApiPassword)) != 1 { - return false - } - return true -} - func setAuthFailed(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", `Basic realm="auth"`) w.WriteHeader(401) @@ -52,11 +36,22 @@ func toJSON(w http.ResponseWriter, o interface{}) { json.NewEncoder(w).Encode(o) } -func showSession(w http.ResponseWriter, r *http.Request) { +func (api *RestAPI) checkAuth(r *http.Request) bool { + user, pass, _ := r.BasicAuth() + // timing attack my ass + if subtle.ConstantTimeCompare([]byte(user), []byte(api.username)) != 1 { + return false + } else if subtle.ConstantTimeCompare([]byte(pass), []byte(api.password)) != 1 { + return false + } + return true +} + +func (api *RestAPI) showSession(w http.ResponseWriter, r *http.Request) { toJSON(w, session.I) } -func runSessionCommand(w http.ResponseWriter, r *http.Request) { +func (api *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) { var err error var cmd CommandRequest @@ -71,7 +66,7 @@ func runSessionCommand(w http.ResponseWriter, r *http.Request) { } } -func showEvents(w http.ResponseWriter, r *http.Request) { +func (api *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) { var err error events := session.I.Events.Sorted() @@ -94,33 +89,33 @@ func showEvents(w http.ResponseWriter, r *http.Request) { toJSON(w, events[0:n]) } -func clearEvents(w http.ResponseWriter, r *http.Request) { +func (api *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) { session.I.Events.Clear() } -func SessionRoute(w http.ResponseWriter, r *http.Request) { +func (api *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) { setSecurityHeaders(w) - if checkAuth(r) == false { + if api.checkAuth(r) == false { setAuthFailed(w) } else if r.Method == "GET" { - showSession(w, r) + api.showSession(w, r) } else if r.Method == "POST" { - runSessionCommand(w, r) + api.runSessionCommand(w, r) } else { http.Error(w, "Bad Request", 400) } } -func EventsRoute(w http.ResponseWriter, r *http.Request) { +func (api *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) { setSecurityHeaders(w) - if checkAuth(r) == false { + if api.checkAuth(r) == false { setAuthFailed(w) } else if r.Method == "GET" { - showEvents(w, r) + api.showEvents(w, r) } else if r.Method == "DELETE" { - clearEvents(w, r) + api.clearEvents(w, r) } else { http.Error(w, "Bad Request", 400) } From b243e67828e786f3148673a15722308ab02f9321 Mon Sep 17 00:00:00 2001 From: evilsocket Date: Thu, 1 Mar 2018 19:30:10 +0100 Subject: [PATCH 2/3] some progress but still doesn't work --- modules/api_rest.go | 26 ++++-- modules/api_rest_controller.go | 143 ++++++++++++++++++++++++++++----- modules/events_stream.go | 13 +-- session/events.go | 18 ++++- 4 files changed, 168 insertions(+), 32 deletions(-) diff --git a/modules/api_rest.go b/modules/api_rest.go index 62f3eabf..3033a488 100644 --- a/modules/api_rest.go +++ b/modules/api_rest.go @@ -10,23 +10,33 @@ import ( "github.com/bettercap/bettercap/log" "github.com/bettercap/bettercap/session" "github.com/bettercap/bettercap/tls" + + "github.com/gorilla/websocket" ) type RestAPI struct { session.SessionModule - server *http.Server - username string - password string - certFile string - keyFile string - useWebsocket bool + server *http.Server + username string + password string + certFile string + keyFile string + useWebsocket bool + upgrader websocket.Upgrader + eventListener <-chan session.Event + quit chan bool } func NewRestAPI(s *session.Session) *RestAPI { api := &RestAPI{ SessionModule: session.NewSessionModule("api.rest", s), server: &http.Server{}, + quit: make(chan bool), useWebsocket: false, + upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, } api.AddParam(session.NewStringParameter("api.rest.address", @@ -163,6 +173,10 @@ func (api *RestAPI) Start() error { func (api *RestAPI) Stop() error { return api.SetRunning(false, func() { + go func() { + api.quit <- true + }() + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() api.server.Shutdown(ctx) diff --git a/modules/api_rest_controller.go b/modules/api_rest_controller.go index 717e1f74..d6eee401 100644 --- a/modules/api_rest_controller.go +++ b/modules/api_rest_controller.go @@ -5,8 +5,21 @@ import ( "encoding/json" "net/http" "strconv" + "time" + "github.com/bettercap/bettercap/log" "github.com/bettercap/bettercap/session" + + "github.com/gorilla/websocket" +) + +const ( + // Time allowed to write an event to the client. + writeWait = 10 * time.Second + // Time allowed to read the next pong message from the client. + pongWait = 60 * time.Second + // Send pings to client with this period. Must be less than pongWait. + pingPeriod = (pongWait * 9) / 10 ) type CommandRequest struct { @@ -18,7 +31,9 @@ type APIResponse struct { Message string `json:"msg"` } -func setAuthFailed(w http.ResponseWriter) { +func setAuthFailed(w http.ResponseWriter, r *http.Request) { + log.Warning("Unauthorized authentication attempt from %s", r.RemoteAddr) + w.Header().Set("WWW-Authenticate", `Basic realm="auth"`) w.WriteHeader(401) w.Write([]byte("Unauthorized")) @@ -66,27 +81,119 @@ func (api *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) { } } -func (api *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) { - var err error +func (api *RestAPI) streamWriter(ws *websocket.Conn, w http.ResponseWriter, r *http.Request) { + defer ws.Close() + // first we stream what we already have events := session.I.Events.Sorted() - nmax := len(events) - n := nmax - - q := r.URL.Query() - vals := q["n"] - if len(vals) > 0 { - n, err = strconv.Atoi(q["n"][0]) - if err == nil { - if n > nmax { - n = nmax + n := len(events) + if n > 0 { + log.Info("Sending %d events.", n) + for _, event := range events { + msg, err := json.Marshal(event) + if err != nil { + log.Error("Error while creating websocket message: %s", err) + return + } + + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Error("Error while writing websocket message: %s", err) + return } - } else { - n = nmax } } - toJSON(w, events[0:n]) + session.I.Events.Clear() + + log.Info("Listening for events and streaming to ws endpoint ...") + + api.eventListener = api.Session.Events.Listen() + + pingTicker := time.NewTicker(pingPeriod) + + for { + select { + case <-pingTicker.C: + ws.SetWriteDeadline(time.Now().Add(writeWait)) + log.Info("Ping") + if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + log.Error("Error while writing websocket ping message: %s", err) + return + } + case event := <-api.eventListener: + log.Info("Event") + msg, err := json.Marshal(event) + if err != nil { + log.Error("Error while creating websocket message: %s", err) + continue + } + + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil { + log.Error("Error while writing websocket message: %s", err) + return + } + log.Info("Sent") + + case <-api.quit: + log.Info("Quit") + return + } + } +} + +func (api *RestAPI) streamReader(ws *websocket.Conn) { + defer ws.Close() + ws.SetReadLimit(512) + ws.SetReadDeadline(time.Now().Add(pongWait)) + ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil }) + for { + _, _, err := ws.ReadMessage() + if err != nil { + log.Info("Closing reader.") + break + } + } +} + +func (api *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) { + var err error + + if api.useWebsocket { + ws, err := api.upgrader.Upgrade(w, r, nil) + if err != nil { + if _, ok := err.(websocket.HandshakeError); !ok { + log.Error("Error while updating api.rest connection to websocket: %s", err) + } + return + } + + log.Info("Websocket streaming started for %s", r.RemoteAddr) + + go api.streamWriter(ws, w, r) + api.streamReader(ws) + } else { + + events := session.I.Events.Sorted() + nmax := len(events) + n := nmax + + q := r.URL.Query() + vals := q["n"] + if len(vals) > 0 { + n, err = strconv.Atoi(q["n"][0]) + if err == nil { + if n > nmax { + n = nmax + } + } else { + n = nmax + } + } + + toJSON(w, events[0:n]) + } } func (api *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) { @@ -97,7 +204,7 @@ func (api *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) { setSecurityHeaders(w) if api.checkAuth(r) == false { - setAuthFailed(w) + setAuthFailed(w, r) } else if r.Method == "GET" { api.showSession(w, r) } else if r.Method == "POST" { @@ -111,7 +218,7 @@ func (api *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) { setSecurityHeaders(w) if api.checkAuth(r) == false { - setAuthFailed(w) + setAuthFailed(w, r) } else if r.Method == "GET" { api.showEvents(w, r) } else if r.Method == "DELETE" { diff --git a/modules/events_stream.go b/modules/events_stream.go index 70e93b73..99d009e2 100644 --- a/modules/events_stream.go +++ b/modules/events_stream.go @@ -12,10 +12,11 @@ import ( type EventsStream struct { session.SessionModule - ignoreList *IgnoreList - waitFor string - waitChan chan *session.Event - quit chan bool + ignoreList *IgnoreList + waitFor string + waitChan chan *session.Event + eventListener <-chan session.Event + quit chan bool } func NewEventsStream(s *session.Session) *EventsStream { @@ -124,10 +125,12 @@ func (s *EventsStream) Configure() error { func (s *EventsStream) Start() error { return s.SetRunning(true, func() { + + s.eventListener = s.Session.Events.Listen() for { var e session.Event select { - case e = <-s.Session.Events.NewEvents: + case e = <-s.eventListener: if e.Tag == s.waitFor { s.waitFor = "" s.waitChan <- &e diff --git a/session/events.go b/session/events.go index 26e719f5..cfd4f605 100644 --- a/session/events.go +++ b/session/events.go @@ -39,21 +39,29 @@ func (e Event) Label() string { type EventPool struct { sync.Mutex - NewEvents chan Event debug bool silent bool events []Event + listeners []chan Event } func NewEventPool(debug bool, silent bool) *EventPool { return &EventPool{ - NewEvents: make(chan Event, 0xff), debug: debug, silent: silent, events: make([]Event, 0), + listeners: make([]chan Event, 0), } } +func (p *EventPool) Listen() <-chan Event { + p.Lock() + defer p.Unlock() + l := make(chan Event) + p.listeners = append(p.listeners, l) + return l +} + func (p *EventPool) SetSilent(s bool) { p.Lock() defer p.Unlock() @@ -71,7 +79,11 @@ func (p *EventPool) Add(tag string, data interface{}) { defer p.Unlock() e := NewEvent(tag, data) p.events = append([]Event{e}, p.events...) - p.NewEvents <- e + + // broadcast the event to every listener + for _, l := range p.listeners { + l <- e + } } func (p *EventPool) Log(level int, format string, args ...interface{}) { From 5ad1a17118d7def6d48a47183715a6010ae2c97d Mon Sep 17 00:00:00 2001 From: evilsocket Date: Tue, 6 Mar 2018 13:54:37 +0100 Subject: [PATCH 3/3] new: implemented optional websocket for /api/events --- modules/api_rest.go | 1 + modules/api_rest_controller.go | 68 ++++++++++++++++++---------------- network/ble_device.go | 25 ++++++++++++- session/events.go | 8 +++- 4 files changed, 66 insertions(+), 36 deletions(-) diff --git a/modules/api_rest.go b/modules/api_rest.go index 3033a488..28c504f7 100644 --- a/modules/api_rest.go +++ b/modules/api_rest.go @@ -33,6 +33,7 @@ func NewRestAPI(s *session.Session) *RestAPI { server: &http.Server{}, quit: make(chan bool), useWebsocket: false, + eventListener: s.Events.Listen(), upgrader: websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, diff --git a/modules/api_rest_controller.go b/modules/api_rest_controller.go index d6eee401..53480ea8 100644 --- a/modules/api_rest_controller.go +++ b/modules/api_rest_controller.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "strconv" + "strings" "time" "github.com/bettercap/bettercap/log" @@ -81,6 +82,33 @@ func (api *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) { } } +func (api *RestAPI) streamEvent(ws *websocket.Conn, event session.Event) error { + msg, err := json.Marshal(event) + if err != nil { + log.Error("Error while creating websocket message: %s", err) + return err + } + + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil { + if !strings.Contains(err.Error(), "closed connection") { + log.Error("Error while writing websocket message: %s", err) + return err + } + } + + return nil +} + +func (api *RestAPI) sendPing(ws *websocket.Conn) error { + ws.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + log.Error("Error while writing websocket ping message: %s", err) + return err + } + return nil +} + func (api *RestAPI) streamWriter(ws *websocket.Conn, w http.ResponseWriter, r *http.Request) { defer ws.Close() @@ -88,17 +116,9 @@ func (api *RestAPI) streamWriter(ws *websocket.Conn, w http.ResponseWriter, r *h events := session.I.Events.Sorted() n := len(events) if n > 0 { - log.Info("Sending %d events.", n) + log.Debug("Sending %d events.", n) for _, event := range events { - msg, err := json.Marshal(event) - if err != nil { - log.Error("Error while creating websocket message: %s", err) - return - } - - ws.SetWriteDeadline(time.Now().Add(writeWait)) - if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil { - log.Error("Error while writing websocket message: %s", err) + if err := api.streamEvent(ws, event); err != nil { return } } @@ -106,38 +126,22 @@ func (api *RestAPI) streamWriter(ws *websocket.Conn, w http.ResponseWriter, r *h session.I.Events.Clear() - log.Info("Listening for events and streaming to ws endpoint ...") - - api.eventListener = api.Session.Events.Listen() + log.Debug("Listening for events and streaming to ws endpoint ...") pingTicker := time.NewTicker(pingPeriod) for { select { case <-pingTicker.C: - ws.SetWriteDeadline(time.Now().Add(writeWait)) - log.Info("Ping") - if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { - log.Error("Error while writing websocket ping message: %s", err) + if err := api.sendPing(ws); err != nil { return } case event := <-api.eventListener: - log.Info("Event") - msg, err := json.Marshal(event) - if err != nil { - log.Error("Error while creating websocket message: %s", err) - continue - } - - ws.SetWriteDeadline(time.Now().Add(writeWait)) - if err := ws.WriteMessage(websocket.TextMessage, msg); err != nil { - log.Error("Error while writing websocket message: %s", err) + if err := api.streamEvent(ws, event); err != nil { return } - log.Info("Sent") - case <-api.quit: - log.Info("Quit") + log.Info("Stopping websocket events streamer ...") return } } @@ -151,7 +155,7 @@ func (api *RestAPI) streamReader(ws *websocket.Conn) { for { _, _, err := ws.ReadMessage() if err != nil { - log.Info("Closing reader.") + log.Debug("Closing websocket reader.") break } } @@ -169,7 +173,7 @@ func (api *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) { return } - log.Info("Websocket streaming started for %s", r.RemoteAddr) + log.Debug("Websocket streaming started for %s", r.RemoteAddr) go api.streamWriter(ws, w, r) api.streamReader(ws) diff --git a/network/ble_device.go b/network/ble_device.go index 50bf097a..40a72199 100644 --- a/network/ble_device.go +++ b/network/ble_device.go @@ -4,6 +4,7 @@ package network import ( + "encoding/json" "time" "github.com/bettercap/gatt" @@ -11,10 +12,18 @@ import ( type BLEDevice struct { LastSeen time.Time - Device gatt.Peripheral Vendor string - Advertisement *gatt.Advertisement RSSI int + Device gatt.Peripheral + Advertisement *gatt.Advertisement +} + +type bleDeviceJSON struct { + LastSeen time.Time `json:"last_seen"` + Name string `json:"name"` + MAC string `json:"mac"` + Vendor string `json:"vendor"` + RSSI int `json:"rssi"` } func NewBLEDevice(p gatt.Peripheral, a *gatt.Advertisement, rssi int) *BLEDevice { @@ -26,3 +35,15 @@ func NewBLEDevice(p gatt.Peripheral, a *gatt.Advertisement, rssi int) *BLEDevice RSSI: rssi, } } + +func (d *BLEDevice) MarshalJSON() ([]byte, error) { + doc := bleDeviceJSON{ + LastSeen: d.LastSeen, + Name: d.Device.Name(), + MAC: d.Device.ID(), + Vendor: d.Vendor, + RSSI: d.RSSI, + } + + return json.Marshal(doc) +} diff --git a/session/events.go b/session/events.go index cfd4f605..24fe1899 100644 --- a/session/events.go +++ b/session/events.go @@ -57,7 +57,7 @@ func NewEventPool(debug bool, silent bool) *EventPool { func (p *EventPool) Listen() <-chan Event { p.Lock() defer p.Unlock() - l := make(chan Event) + l := make(chan Event, 1) p.listeners = append(p.listeners, l) return l } @@ -77,12 +77,16 @@ func (p *EventPool) SetDebug(d bool) { func (p *EventPool) Add(tag string, data interface{}) { p.Lock() defer p.Unlock() + e := NewEvent(tag, data) p.events = append([]Event{e}, p.events...) // broadcast the event to every listener for _, l := range p.listeners { - l <- e + select { + case l <- e: + default: + } } }