From 1cbe4c271b31f6006a93b26228dece6de30e3cf1 Mon Sep 17 00:00:00 2001 From: Matthias Hochgatterer Date: Mon, 12 Oct 2020 17:19:12 +0200 Subject: [PATCH] Send notifications only to subscribed clients --- hap/controller/characteristic_controller.go | 111 ------------ .../characteristic_controller_test.go | 122 ------------- hap/controller/container.go | 31 ---- hap/controller/doc.go | 2 - hap/endpoint/accessories.go | 55 ------ hap/endpoint/characteristics.go | 70 -------- hap/endpoint/identify.go | 24 --- hap/handler.go | 19 -- hap/http/accessories.go | 24 +++ hap/http/characteristics.go | 163 ++++++++++++++++++ hap/http/err.go | 6 + hap/http/identify.go | 21 +++ hap/http/json.go | 44 +++++ hap/http/listener.go | 26 +++ hap/http/server.go | 54 ++++-- hap/listener.go | 31 ---- hap/session.go | 19 ++ ip_transport.go | 20 ++- password.go | 4 +- password_test.go | 16 +- 20 files changed, 365 insertions(+), 497 deletions(-) delete mode 100644 hap/controller/characteristic_controller.go delete mode 100644 hap/controller/characteristic_controller_test.go delete mode 100644 hap/controller/container.go delete mode 100644 hap/controller/doc.go delete mode 100644 hap/endpoint/accessories.go delete mode 100644 hap/endpoint/characteristics.go delete mode 100644 hap/endpoint/identify.go create mode 100644 hap/http/accessories.go create mode 100644 hap/http/characteristics.go create mode 100644 hap/http/err.go create mode 100644 hap/http/identify.go create mode 100644 hap/http/json.go create mode 100644 hap/http/listener.go delete mode 100644 hap/listener.go diff --git a/hap/controller/characteristic_controller.go b/hap/controller/characteristic_controller.go deleted file mode 100644 index 9e015253..00000000 --- a/hap/controller/characteristic_controller.go +++ /dev/null @@ -1,111 +0,0 @@ -package controller - -import ( - "github.com/brutella/hc/accessory" - "github.com/brutella/hc/characteristic" - "github.com/brutella/hc/hap" - "github.com/brutella/hc/hap/data" - "github.com/brutella/hc/log" - "github.com/xiam/to" - - "bytes" - "encoding/json" - - "io" - "io/ioutil" - "net" - "net/url" - "strings" -) - -// CharacteristicController implements the CharacteristicsHandler interface and provides -// read (GET) and write (POST) interfaces to the managed characteristics. -type CharacteristicController struct { - container *accessory.Container -} - -// NewCharacteristicController returns a new characteristic controller. -func NewCharacteristicController(m *accessory.Container) *CharacteristicController { - return &CharacteristicController{container: m} -} - -// HandleGetCharacteristics handles a get characteristic request like `/characteristics?id=1.4,1.5` -func (ctr *CharacteristicController) HandleGetCharacteristics(form url.Values, conn net.Conn) (io.Reader, error) { - var b bytes.Buffer - var chs []data.Characteristic - - // id=1.4,1.5 - paths := strings.Split(form.Get("id"), ",") - for _, p := range paths { - if ids := strings.Split(p, "."); len(ids) == 2 { - aid := to.Uint64(ids[0]) // accessory id - iid := to.Uint64(ids[1]) // instance id (= characteristic id) - c := data.Characteristic{AccessoryID: aid, CharacteristicID: iid} - if ch := ctr.GetCharacteristic(aid, iid); ch != nil { - c.Value = ch.GetValueFromConnection(conn) - } else { - c.Status = hap.StatusServiceCommunicationFailure - } - chs = append(chs, c) - } - } - - result, err := json.Marshal(&data.Characteristics{chs}) - if err != nil { - log.Info.Panic(err) - } - - b.Write(result) - return &b, err -} - -// HandleUpdateCharacteristics handles an update characteristic request. The bytes must represent -// a data.Characteristics json. -func (ctr *CharacteristicController) HandleUpdateCharacteristics(r io.Reader, conn net.Conn) error { - b, err := ioutil.ReadAll(r) - if err != nil { - return err - } - - var chars data.Characteristics - err = json.Unmarshal(b, &chars) - if err != nil { - return err - } - - log.Debug.Println(string(b)) - - for _, c := range chars.Characteristics { - characteristic := ctr.GetCharacteristic(c.AccessoryID, c.CharacteristicID) - if characteristic == nil { - log.Info.Printf("Could not find characteristic with aid %d and iid %d\n", c.AccessoryID, c.CharacteristicID) - continue - } - - if c.Value != nil { - characteristic.UpdateValueFromConnection(c.Value, conn) - } - - if events, ok := c.Events.(bool); ok == true { - characteristic.Events = events - } - } - - return err -} - -// GetCharacteristic returns the characteristic identified by the accessory id aid and characteristic id iid -func (ctr *CharacteristicController) GetCharacteristic(aid uint64, iid uint64) *characteristic.Characteristic { - for _, a := range ctr.container.Accessories { - if a.ID == aid { - for _, s := range a.GetServices() { - for _, c := range s.GetCharacteristics() { - if c.ID == iid { - return c - } - } - } - } - } - return nil -} diff --git a/hap/controller/characteristic_controller_test.go b/hap/controller/characteristic_controller_test.go deleted file mode 100644 index ee3b125f..00000000 --- a/hap/controller/characteristic_controller_test.go +++ /dev/null @@ -1,122 +0,0 @@ -package controller - -import ( - "github.com/brutella/hc/accessory" - "github.com/brutella/hc/characteristic" - "github.com/brutella/hc/hap/data" - "github.com/brutella/hc/service" - - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/url" - "testing" -) - -func idsString(accessoryID, characteristicID uint64) url.Values { - values := url.Values{} - values.Set("id", fmt.Sprintf("%d.%d", accessoryID, characteristicID)) - - return values -} - -func TestGetCharacteristic(t *testing.T) { - info := accessory.Info{ - Name: "My Bridge", - SerialNumber: "001", - Manufacturer: "Google", - Model: "Bridge", - } - - a := accessory.New(info, accessory.TypeBridge) - - m := accessory.NewContainer() - m.AddAccessory(a) - - values := idsString(a.ID, a.Info.Name.ID) - controller := NewCharacteristicController(m) - res, err := controller.HandleGetCharacteristics(values, characteristic.TestConn) - - if err != nil { - t.Fatal(err) - } - - b, err := ioutil.ReadAll(res) - - if err != nil { - t.Fatal(err) - } - - var chars data.Characteristics - err = json.Unmarshal(b, &chars) - - if err != nil { - t.Fatal(err) - } - - if is, want := len(chars.Characteristics), 1; is != want { - t.Fatalf("is=%v want=%v", is, want) - } - if x := chars.Characteristics[0].Value; x != "My Bridge" { - t.Fatal(x) - } -} - -func toSwitchService(obj interface{}) *service.Switch { - return obj.(*service.Switch) -} - -func TestPutCharacteristic(t *testing.T) { - info := accessory.Info{ - Name: "My Switch", - SerialNumber: "001", - Manufacturer: "Google", - Model: "Bridge", - } - - a := accessory.NewSwitch(info) - a.Switch.On.SetValue(false) - - m := accessory.NewContainer() - m.AddAccessory(a.Accessory) - - // find on characteristic with type TypeOn - var cid uint64 - for _, s := range a.Accessory.Services { - for _, c := range s.Characteristics { - if c.Type == characteristic.TypeOn { - cid = c.ID - } - } - } - - if cid == 0 { - t.Fatal("characteristic not found") - } - - char := data.Characteristic{AccessoryID: 1, CharacteristicID: cid, Value: true} - var slice []data.Characteristic - slice = append(slice, char) - - chars := data.Characteristics{Characteristics: slice} - b, err := json.Marshal(chars) - - if err != nil { - t.Fatal(err) - } - - var buffer bytes.Buffer - buffer.Write(b) - - controller := NewCharacteristicController(m) - err = controller.HandleUpdateCharacteristics(&buffer, characteristic.TestConn) - - if err != nil { - t.Fatal(err) - } - - if is, want := a.Switch.On.GetValue(), true; is != want { - t.Fatalf("is=%v want=%v", is, want) - } -} diff --git a/hap/controller/container.go b/hap/controller/container.go deleted file mode 100644 index 5cafc5e9..00000000 --- a/hap/controller/container.go +++ /dev/null @@ -1,31 +0,0 @@ -package controller - -import ( - "bytes" - "encoding/json" - "github.com/brutella/hc/accessory" - "io" -) - -// ContainerController implements the AccessoriesHandler interface. -type ContainerController struct { - container *accessory.Container -} - -// NewContainerController returns a controller for the argument container. -func NewContainerController(m *accessory.Container) *ContainerController { - return &ContainerController{container: m} -} - -// HandleGetAccessories returns the container as json bytes. -func (ctr *ContainerController) HandleGetAccessories(r io.Reader) (io.Reader, error) { - result, err := json.Marshal(ctr.container) - return bytes.NewBuffer(result), err -} - -// IdentifyAccessory calls Identify() for all accessories. -func (ctr *ContainerController) IdentifyAccessory() { - for _, a := range ctr.container.Accessories { - a.Identify() - } -} diff --git a/hap/controller/doc.go b/hap/controller/doc.go deleted file mode 100644 index 52a98e65..00000000 --- a/hap/controller/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package controller implements the handler interfaces to access the model. -package controller diff --git a/hap/endpoint/accessories.go b/hap/endpoint/accessories.go deleted file mode 100644 index 4b517cb1..00000000 --- a/hap/endpoint/accessories.go +++ /dev/null @@ -1,55 +0,0 @@ -package endpoint - -import ( - "github.com/brutella/hc/hap" - "github.com/brutella/hc/log" - - "io/ioutil" - "net/http" - "sync" -) - -// Accessories handles the /accessories endpoint and returns all accessories as JSON -// -// This endpoint is not session based and the same for all connections because -// the encryption/decryption is handled by the connection automatically. -type Accessories struct { - http.Handler - - controller hap.AccessoriesHandler - mutex *sync.Mutex -} - -// NewAccessories returns a new handler for accessories endpoint -func NewAccessories(c hap.AccessoriesHandler, mutex *sync.Mutex) *Accessories { - handler := Accessories{ - controller: c, - mutex: mutex, - } - - return &handler -} - -func (handler *Accessories) ServeHTTP(response http.ResponseWriter, request *http.Request) { - log.Debug.Printf("%v GET /accessories", request.RemoteAddr) - response.Header().Set("Content-Type", hap.HTTPContentTypeHAPJson) - - handler.mutex.Lock() - res, err := handler.controller.HandleGetAccessories(request.Body) - handler.mutex.Unlock() - - if err != nil { - log.Info.Panic(err) - response.WriteHeader(http.StatusInternalServerError) - } else { - // Write the data in chunks of 2048 bytes - // http.ResponseWriter should do this already, but crashes because of an unkown reason - wr := hap.NewChunkedWriter(response, 2048) - b, _ := ioutil.ReadAll(res) - log.Debug.Println(string(b)) - _, err := wr.Write(b) - if err != nil { - log.Info.Panic(err) - } - } -} diff --git a/hap/endpoint/characteristics.go b/hap/endpoint/characteristics.go deleted file mode 100644 index d3ff1d0d..00000000 --- a/hap/endpoint/characteristics.go +++ /dev/null @@ -1,70 +0,0 @@ -package endpoint - -import ( - "github.com/brutella/hc/hap" - "github.com/brutella/hc/log" - "io" - "io/ioutil" - "net/http" - "sync" -) - -// Characteristics handles the /characteristics endpoint -// -// This endpoint is not session based and the same for all connections because -// the encryption/decryption is handled by the connection automatically. -type Characteristics struct { - http.Handler - - controller hap.CharacteristicsHandler - mutex *sync.Mutex - context hap.Context -} - -// NewCharacteristics returns a new handler for characteristics endpoint -func NewCharacteristics(context hap.Context, c hap.CharacteristicsHandler, mutex *sync.Mutex) *Characteristics { - handler := Characteristics{ - controller: c, - mutex: mutex, - context: context, - } - - return &handler -} - -func (handler *Characteristics) ServeHTTP(response http.ResponseWriter, request *http.Request) { - var res io.Reader - var err error - - handler.mutex.Lock() - switch request.Method { - case hap.MethodGET: - request.ParseForm() - log.Debug.Printf("%v GET /characteristics %v", request.RemoteAddr, request.Form) - session := handler.context.GetSessionForRequest(request) - conn := session.Connection() - res, err = handler.controller.HandleGetCharacteristics(request.Form, conn) - case hap.MethodPUT: - log.Debug.Printf("%v PUT /characteristics", request.RemoteAddr) - session := handler.context.GetSessionForRequest(request) - conn := session.Connection() - err = handler.controller.HandleUpdateCharacteristics(request.Body, conn) - default: - log.Debug.Println("Cannot handle HTTP method", request.Method) - } - handler.mutex.Unlock() - - if err != nil { - log.Info.Panic(err) - response.WriteHeader(http.StatusInternalServerError) - } else { - if res != nil { - response.Header().Set("Content-Type", hap.HTTPContentTypeHAPJson) - wr := hap.NewChunkedWriter(response, 2048) - b, _ := ioutil.ReadAll(res) - wr.Write(b) - } else { - response.WriteHeader(http.StatusNoContent) - } - } -} diff --git a/hap/endpoint/identify.go b/hap/endpoint/identify.go deleted file mode 100644 index 5161ac98..00000000 --- a/hap/endpoint/identify.go +++ /dev/null @@ -1,24 +0,0 @@ -package endpoint - -import ( - "github.com/brutella/hc/hap" - "github.com/brutella/hc/log" - "net/http" -) - -// Identify handles the unencrypted /identify endpoint by calling IdentifyAccessory() on the IdentifyHandler -type Identify struct { - http.Handler - handler hap.IdentifyHandler -} - -// NewIdentify returns an object which serves the /identify endpoint -func NewIdentify(h hap.IdentifyHandler) *Identify { - return &Identify{handler: h} -} - -func (i *Identify) ServeHTTP(response http.ResponseWriter, request *http.Request) { - log.Debug.Printf("%v POST /identify", request.RemoteAddr) - i.handler.IdentifyAccessory() - response.WriteHeader(http.StatusNoContent) -} diff --git a/hap/handler.go b/hap/handler.go index 05b4923f..dad6f494 100644 --- a/hap/handler.go +++ b/hap/handler.go @@ -2,9 +2,6 @@ package hap import ( "github.com/brutella/hc/util" - "io" - "net" - "net/url" ) // A ContainerHandler abstracts request/response communication @@ -17,19 +14,3 @@ type PairVerifyHandler interface { ContainerHandler SharedKey() [32]byte } - -// A AccessoriesHandler returns a list of accessories as json. -type AccessoriesHandler interface { - HandleGetAccessories(r io.Reader) (io.Reader, error) -} - -// A CharacteristicsHandler handles get and update characteristic. -type CharacteristicsHandler interface { - HandleGetCharacteristics(url.Values, net.Conn) (io.Reader, error) - HandleUpdateCharacteristics(io.Reader, net.Conn) error -} - -// IdentifyHandler calls Identify() on accessories. -type IdentifyHandler interface { - IdentifyAccessory() -} diff --git a/hap/http/accessories.go b/hap/http/accessories.go new file mode 100644 index 00000000..680b1c97 --- /dev/null +++ b/hap/http/accessories.go @@ -0,0 +1,24 @@ +package http + +import ( + "github.com/brutella/hc/hap" + "github.com/brutella/hc/log" + + "net/http" +) + +func (srv *Server) Accessories(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case hap.MethodGET: + log.Debug.Printf("%v GET /accessories", r.RemoteAddr) + + srv.mutex.Lock() + if err := WriteJSON(w, r, srv.container); err != nil { + log.Info.Println(err) + } + srv.mutex.Unlock() + + default: + log.Debug.Println("Cannot handle HTTP method", r.Method) + } +} diff --git a/hap/http/characteristics.go b/hap/http/characteristics.go new file mode 100644 index 00000000..0855fa63 --- /dev/null +++ b/hap/http/characteristics.go @@ -0,0 +1,163 @@ +package http + +import ( + "github.com/brutella/hc/hap" + "github.com/brutella/hc/log" + "github.com/xiam/to" + + "bytes" + "io/ioutil" + "net/http" + "strings" +) + +type CharacteristicsResponse struct { + Characteristics []CharacteristicResponse `json:"characteristics"` +} + +type CharacteristicResponse struct { + AccessoryID uint64 `json:"aid"` + CharacteristicID uint64 `json:"iid"` + Value interface{} `json:"value,omitempty"` + + // Status contains the status code. Should be interpreted as integer. + // The property is omited if not specified, which makes the payload smaller. + Status *int `json:"status,omitempty"` +} + +type CharacteristicRequest struct { + AccessoryID uint64 `json:"aid"` + CharacteristicID uint64 `json:"iid"` + Value interface{} `json:"value"` + Events interface{} `json:"ev,omitempty"` +} + +// Authenticate verfies that a sesson for the request is available. +func (srv *Server) Authenticate(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", hap.HTTPContentTypeHAPJson) + sess := srv.context.GetSessionForRequest(r) + if sess == nil { + w.WriteHeader(470) // this custom status code indicates an error + if err := WriteJSON(w, r, &ErrResponse{Status: hap.StatusInsufficientPrivileges}); err != nil { + log.Debug.Println(err) + } + } + + next.ServeHTTP(w, r) + }) +} + +// Characteristics Handles GET and PUT requests for the /characteristics endpoint. +func (srv *Server) Characteristics(w http.ResponseWriter, r *http.Request) { + sess := srv.context.GetSessionForRequest(r) + conn := sess.Connection() + + switch r.Method { + case hap.MethodGET: + r.ParseForm() + log.Debug.Printf("%v GET /characteristics %v", r.RemoteAddr, r.Form) + + var ( + err = false // indicates if a characteristic was not found + arr []CharacteristicResponse + ) + + // id=1.4,1.5 + strs := strings.Split(r.Form.Get("id"), ",") + for _, str := range strs { + if ids := strings.Split(str, "."); len(ids) == 2 { + aid := to.Uint64(ids[0]) // accessory id + iid := to.Uint64(ids[1]) // instance id (= characteristic id) + resp := CharacteristicResponse{AccessoryID: aid, CharacteristicID: iid} + if ch := srv.getCharacteristic(aid, iid); ch != nil { + resp.Value = ch.GetValueFromConnection(conn) + } else { + err = true + status := hap.StatusServiceCommunicationFailure + resp.Status = &status + } + arr = append(arr, resp) + } else { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + if err == true { + // Set 207 status when any of the response includes an error + w.WriteHeader(http.StatusMultiStatus) + for _, resp := range arr { + if resp.Status == nil { + ok := 0 + resp.Status = &ok // make sure that every response contains a status code (0 means OK) + } + } + } else { + w.WriteHeader(http.StatusOK) + } + + WriteJSON(w, r, &CharacteristicsResponse{arr}) + case hap.MethodPUT: + log.Debug.Printf("%v PUT /characteristics", r.RemoteAddr) + b, err := ioutil.ReadAll(r.Body) + if err != nil { + log.Debug.Println(err) + } else { + log.Debug.Println(string(b)) + } + + req := struct { + Characteristics []CharacteristicRequest `json:"characteristics"` + }{} + err = JSONDecode(bytes.NewBuffer(b), &req) + if err != nil { + log.Debug.Println(err) + w.WriteHeader(http.StatusInternalServerError) + if err := WriteJSON(w, r, &ErrResponse{Status: hap.StatusInvalidValueInRequest /*?*/}); err != nil { + log.Debug.Println(err) + } + return + } + + resp := &CharacteristicsResponse{} + for _, ch := range req.Characteristics { + c := srv.getCharacteristic(ch.AccessoryID, ch.CharacteristicID) + if c == nil { + log.Info.Printf("Could not find characteristic with aid %d and iid %d\n", ch.AccessoryID, ch.CharacteristicID) + continue + } + + if ch.Value != nil { + c.UpdateValueFromConnection(ch.Value, conn) + } + + if ch.Events != nil { + if !c.IsObservable() { + status := hap.StatusNotificationNotSupported + err := CharacteristicResponse{AccessoryID: ch.AccessoryID, CharacteristicID: ch.CharacteristicID, Status: &status} + resp.Characteristics = append(resp.Characteristics, err) + continue + } + + if events, ok := ch.Events.(bool); ok == true { + if events { + sess.Subscribe(c) + } else { + sess.Unsubscribe(c) + } + } + } + } + + if len(resp.Characteristics) == 0 { + w.WriteHeader(http.StatusNoContent) + return + } + + WriteJSON(w, r, resp) + + default: + log.Debug.Println("Cannot handle HTTP method", r.Method) + } +} diff --git a/hap/http/err.go b/hap/http/err.go new file mode 100644 index 00000000..d162c04c --- /dev/null +++ b/hap/http/err.go @@ -0,0 +1,6 @@ +package http + +// ErrResponse is an error response. +type ErrResponse struct { + Status int `json:status` +} diff --git a/hap/http/identify.go b/hap/http/identify.go new file mode 100644 index 00000000..e5cbc022 --- /dev/null +++ b/hap/http/identify.go @@ -0,0 +1,21 @@ +package http + +import ( + "github.com/brutella/hc/log" + + "net/http" +) + +// TODO respond with 400 if accessory is already paired +// +// HTTP/1.1 400 Bad Request +// Content-Type: application/hap+json +// Content-Length: +// { "status" : -70401 } +func (srv *Server) Identify(w http.ResponseWriter, r *http.Request) { + log.Debug.Printf("%v POST /identify", r.RemoteAddr) + for _, a := range srv.container.Accessories { + a.Identify() + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/hap/http/json.go b/hap/http/json.go new file mode 100644 index 00000000..f3c66de0 --- /dev/null +++ b/hap/http/json.go @@ -0,0 +1,44 @@ +package http + +import ( + "github.com/brutella/hc/hap" + + "bytes" + "encoding/json" + "io" + "net/http" +) + +func JSONEncode(v interface{}) (*bytes.Buffer, error) { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(true) + err := enc.Encode(v) + + return buf, err +} + +func JSONDecode(r io.Reader, v interface{}) error { + return json.NewDecoder(r).Decode(v) +} + +func WriteJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { + buf, err := JSONEncode(v) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return err + } + + wr := hap.NewChunkedWriter(w, 2048) + wr.Write(buf.Bytes()) + + return nil +} + +func ReadJSON(w http.ResponseWriter, r *http.Request, v interface{}) error { + if err := JSONDecode(r.Body, v); err != nil { + return err + } + + return nil +} diff --git a/hap/http/listener.go b/hap/http/listener.go new file mode 100644 index 00000000..5ec13051 --- /dev/null +++ b/hap/http/listener.go @@ -0,0 +1,26 @@ +package http + +import ( + "github.com/brutella/hc/hap" + + "net" +) + +func (s *Server) Accept() (con net.Conn, err error) { + con, err = s.listener.AcceptTCP() + if err != nil { + return + } + + hapCon := hap.NewConnection(con, s.context) + + return hapCon, err +} + +func (s *Server) Close() error { + return s.listener.Close() +} + +func (s *Server) Addr() net.Addr { + return s.listener.Addr() +} diff --git a/hap/http/server.go b/hap/http/server.go index 84b90428..0fae5b8e 100644 --- a/hap/http/server.go +++ b/hap/http/server.go @@ -2,10 +2,10 @@ package http import ( "github.com/brutella/hc/accessory" + "github.com/brutella/hc/characteristic" "github.com/brutella/hc/db" "github.com/brutella/hc/event" "github.com/brutella/hc/hap" - "github.com/brutella/hc/hap/controller" "github.com/brutella/hc/hap/endpoint" "github.com/brutella/hc/hap/pair" "github.com/brutella/hc/log" @@ -35,9 +35,8 @@ type Server struct { mutex *sync.Mutex container *accessory.Container - port string - listener *net.TCPListener - hapListener *hap.TCPListener + port string + listener *net.TCPListener emitter event.Emitter } @@ -70,6 +69,22 @@ func NewServer(c Config) *Server { return &s } +func testable(c Config) *Server { + s := Server{ + context: c.Context, + database: c.Database, + container: c.Container, + device: c.Device, + Mux: http.NewServeMux(), + mutex: c.Mutex, + emitter: c.Emitter, + } + + s.setupEndpoints() + + return &s +} + func (s *Server) ListenAndServe(ctx context.Context) error { go func() { <-ctx.Done() @@ -77,7 +92,7 @@ func (s *Server) ListenAndServe(ctx context.Context) error { c.Close() } // Stop listener - s.hapListener.Close() + s.listener.Close() }() return s.listenAndServe(s.addrString(), s.Mux, s.context) } @@ -86,13 +101,9 @@ func (s *Server) Port() string { return s.port } -// listenAndServe returns a http.Server to listen on a specific address func (s *Server) listenAndServe(addr string, handler http.Handler, context hap.Context) error { server := http.Server{Addr: addr, Handler: handler} - // Use a TCPListener - listener := hap.NewTCPListener(s.listener, context) - s.hapListener = listener - return server.Serve(listener) + return server.Serve(s) } func (s *Server) addrString() string { @@ -101,14 +112,27 @@ func (s *Server) addrString() string { // setupEndpoints creates controller objects to handle HAP endpoints func (s *Server) setupEndpoints() { - containerController := controller.NewContainerController(s.container) - characteristicsController := controller.NewCharacteristicController(s.container) pairingController := pair.NewPairingController(s.database) s.Mux.Handle("/pair-setup", endpoint.NewPairSetup(s.context, s.device, s.database, s.emitter)) s.Mux.Handle("/pair-verify", endpoint.NewPairVerify(s.context, s.database)) - s.Mux.Handle("/accessories", endpoint.NewAccessories(containerController, s.mutex)) - s.Mux.Handle("/characteristics", endpoint.NewCharacteristics(s.context, characteristicsController, s.mutex)) + s.Mux.Handle("/accessories", s.Authenticate(http.HandlerFunc(s.Accessories))) + s.Mux.Handle("/characteristics", s.Authenticate(http.HandlerFunc(s.Characteristics))) s.Mux.Handle("/pairings", endpoint.NewPairing(pairingController, s.emitter)) - s.Mux.Handle("/identify", endpoint.NewIdentify(containerController)) + s.Mux.HandleFunc("/identify", s.Identify) +} + +func (srv *Server) getCharacteristic(aid uint64, iid uint64) *characteristic.Characteristic { + for _, a := range srv.container.Accessories { + if a.ID == aid { + for _, s := range a.GetServices() { + for _, c := range s.GetCharacteristics() { + if c.ID == iid { + return c + } + } + } + } + } + return nil } diff --git a/hap/listener.go b/hap/listener.go deleted file mode 100644 index dc5561b2..00000000 --- a/hap/listener.go +++ /dev/null @@ -1,31 +0,0 @@ -package hap - -import ( - "net" -) - -// TCPListener listens for new connection and creates Connections for new connections -type TCPListener struct { - *net.TCPListener - context Context -} - -// NewTCPListener returns a new hap tcp listener. -func NewTCPListener(l *net.TCPListener, context Context) *TCPListener { - return &TCPListener{l, context} -} - -// Accept creates and returns a Connection. -func (l *TCPListener) Accept() (c net.Conn, err error) { - conn, err := l.AcceptTCP() - if err != nil { - return - } - - // TODO(brutella) Check if we should use tcp keepalive - // conn.SetKeepAlive(true) - // conn.SetKeepAlivePeriod(3 * time.Minute) - hapConn := NewConnection(conn, l.context) - - return hapConn, err -} diff --git a/hap/session.go b/hap/session.go index 98b7b234..766d336b 100644 --- a/hap/session.go +++ b/hap/session.go @@ -1,6 +1,7 @@ package hap import ( + "github.com/brutella/hc/characteristic" "github.com/brutella/hc/crypto" "net" ) @@ -30,6 +31,10 @@ type Session interface { // Connection returns the associated connection Connection() net.Conn + + Subscribe(*characteristic.Characteristic) + Unsubscribe(*characteristic.Characteristic) + IsSubscribedTo(*characteristic.Characteristic) bool } type session struct { @@ -37,6 +42,7 @@ type session struct { pairStartHandler ContainerHandler pairVerifyHandler PairVerifyHandler connection net.Conn + subs map[*characteristic.Characteristic]bool // Temporary variable to reference next cryptographer nextCryptographer crypto.Cryptographer @@ -46,6 +52,7 @@ type session struct { func NewSession(connection net.Conn) Session { s := session{ connection: connection, + subs: map[*characteristic.Characteristic]bool{}, } return &s @@ -91,3 +98,15 @@ func (s *session) SetPairSetupHandler(c ContainerHandler) { func (s *session) SetPairVerifyHandler(c PairVerifyHandler) { s.pairVerifyHandler = c } + +func (s *session) IsSubscribedTo(ch *characteristic.Characteristic) bool { + return s.subs[ch] == true +} + +func (s *session) Subscribe(ch *characteristic.Characteristic) { + s.subs[ch] = true +} + +func (s *session) Unsubscribe(ch *characteristic.Characteristic) { + s.subs[ch] = false +} diff --git a/ip_transport.go b/ip_transport.go index 44e21437..7253b78d 100644 --- a/ip_transport.go +++ b/ip_transport.go @@ -81,7 +81,7 @@ func NewIPTransport(config Config, a *accessory.Accessory, as ...*accessory.Acce database := db.NewDatabaseWithStorage(storage) - hap_pin, err := NewPin(cfg.Pin) + hap_pin, err := ValidatePin(cfg.Pin) if err != nil { return nil, err } @@ -246,16 +246,12 @@ func (t *ipTransport) addAccessory(a *accessory.Accessory) { // all listeners are notified. Since we don't track which client is interested in // which characteristic change event, we send them to all active connections. onConnChange := func(conn net.Conn, c *characteristic.Characteristic, new, old interface{}) { - if c.Events == true { - t.notifyListener(a, c, conn) - } + t.notifyListener(a, c, conn) } c.OnValueUpdateFromConn(onConnChange) onChange := func(c *characteristic.Characteristic, new, old interface{}) { - if c.Events == true { - t.notifyListener(a, c, nil) - } + t.notifyListener(a, c, nil) } c.OnValueUpdate(onChange) } @@ -268,6 +264,16 @@ func (t *ipTransport) notifyListener(a *accessory.Accessory, c *characteristic.C if conn == except { continue } + + sess := t.context.GetSessionForConnection(conn) + if sess == nil { + continue + } + + if !sess.IsSubscribedTo(c) { + continue + } + resp, err := hap.NewCharacteristicNotification(a, c) if err != nil { log.Info.Panic(err) diff --git a/password.go b/password.go index e8021727..fee27eb9 100644 --- a/password.go +++ b/password.go @@ -8,8 +8,8 @@ import ( var invalidPins = []string{"12345678", "87654321", "00000000", "11111111", "22222222", "33333333", "44444444", "55555555", "66666666", "77777777", "88888888", "99999999"} -// NewPin returns a HomeKit compatible pin string from a 8-numbers strings e.g. '01020304'. -func NewPin(pin string) (string, error) { +// ValidatePin validates a HomeKit pin. +func ValidatePin(pin string) (string, error) { var fmtPin string for _, invalidPin := range invalidPins { if pin == invalidPin { diff --git a/password_test.go b/password_test.go index 6dc6b720..611d2174 100644 --- a/password_test.go +++ b/password_test.go @@ -5,7 +5,7 @@ import ( ) func TestPin(t *testing.T) { - pwd, err := NewPin("00011222") + pwd, err := ValidatePin("00011222") if err != nil { t.Fatal(err) } @@ -15,37 +15,37 @@ func TestPin(t *testing.T) { } func TestShortPin(t *testing.T) { - if _, err := NewPin("0001122"); err == nil { + if _, err := ValidatePin("0001122"); err == nil { t.Fatal("expected error") } } func TestLongPin(t *testing.T) { - if _, err := NewPin("000112221"); err == nil { + if _, err := ValidatePin("000112221"); err == nil { t.Fatal("expected error") } } func TestNonNumberPin(t *testing.T) { - if _, err := NewPin("0001122a"); err == nil { + if _, err := ValidatePin("0001122a"); err == nil { t.Fatal("expected error") } } func TestInvalidPins(t *testing.T) { - if _, err := NewPin("12345678"); err == nil { + if _, err := ValidatePin("12345678"); err == nil { t.Fatal("expected error") } - if _, err := NewPin("87654321"); err == nil { + if _, err := ValidatePin("87654321"); err == nil { t.Fatal("expected error") } - if _, err := NewPin("11111111"); err == nil { + if _, err := ValidatePin("11111111"); err == nil { t.Fatal("expected error") } - if _, err := NewPin("99999999"); err == nil { + if _, err := ValidatePin("99999999"); err == nil { t.Fatal("expected error") } }