diff --git a/README.md b/README.md index 097b481..45b964e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GoDoc Widget]][GoDoc] [![Travis Widget]][Travis] `hap` (previously [hc](https://github.com/brutella/hc)) is a lightweight library to develop HomeKit accessories in Go. -It abstracts the **H**omeKit **A**ccessory **P**rotocol (HAP) and makes it easy to work with [services](service/README.md) and [characteristics](characteristic/README.md). +It abstracts the **H**omeKit **A**ccessory **P**rotocol (HAP) and makes it easy to work with [services](service/README.md) and [characteristics](characteristic). `hap` handles the underlying communication between HomeKit accessories and clients. You can focus on implementing the business logic for your accessory, without having to worry about the protocol. diff --git a/accessory/air_purifier.go b/accessory/air_purifier.go index 18c0589..0c8219a 100644 --- a/accessory/air_purifier.go +++ b/accessory/air_purifier.go @@ -13,6 +13,7 @@ type AirPurifier struct { func NewAirPurifier(info Info) *AirPurifier { a := AirPurifier{} a.A = New(info, TypeAirPurifier) + a.AirPurifier = service.NewAirPurifier() a.AddS(a.AirPurifier.S) diff --git a/accessory/camera.go b/accessory/camera.go index a301c6d..eb8e530 100644 --- a/accessory/camera.go +++ b/accessory/camera.go @@ -16,6 +16,7 @@ type Camera struct { func NewCamera(info Info) *Camera { a := Camera{} a.A = New(info, TypeIPCamera) + a.Control = service.NewCameraControl() a.AddS(a.Control.S) diff --git a/accessory/colored_lightbulb.go b/accessory/colored_lightbulb.go index 3bdc9f1..eafcd97 100644 --- a/accessory/colored_lightbulb.go +++ b/accessory/colored_lightbulb.go @@ -11,13 +11,11 @@ type ColoredLightbulb struct { // NewLightbulb returns an light bulb accessory. func NewColoredLightbulb(info Info) *ColoredLightbulb { - a := New(info, TypeLightbulb) + a := ColoredLightbulb{} + a.A = New(info, TypeLightbulb) - l := service.NewColoredLightbulb() - a.Ss = append(a.Ss, l.S) + a.Lightbulb = service.NewColoredLightbulb() + a.AddS(a.Lightbulb.S) - return &ColoredLightbulb{ - A: a, - Lightbulb: l, - } + return &a } diff --git a/accessory/contact_sensor.go b/accessory/contact_sensor.go new file mode 100644 index 0000000..d002f44 --- /dev/null +++ b/accessory/contact_sensor.go @@ -0,0 +1,19 @@ +package accessory + +import "github.com/brutella/hap/service" + +type ContactSensor struct { + *A + ContactSensor *service.ContactSensor +} + +// NewContactSensor implements a contact sensor. +func NewContactSensor(info Info) *ContactSensor { + a := ContactSensor{} + a.A = New(info, TypeSensor) + + a.ContactSensor = service.NewContactSensor() + a.AddS(a.ContactSensor.S) + + return &a +} diff --git a/accessory/cooler.go b/accessory/cooler.go index 38e86b5..e10c48b 100644 --- a/accessory/cooler.go +++ b/accessory/cooler.go @@ -13,6 +13,7 @@ type Cooler struct { func NewCooler(info Info) *Cooler { a := Cooler{} a.A = New(info, TypeAirConditioner) + a.Cooler = service.NewCooler() a.AddS(a.Cooler.S) diff --git a/accessory/dehumidifier.go b/accessory/dehumidifier.go index 9557ec0..a0e4648 100644 --- a/accessory/dehumidifier.go +++ b/accessory/dehumidifier.go @@ -6,19 +6,16 @@ import ( type Dehumidifier struct { *A - Dehumidifier *service.Dehumidifier } // NewDehumidifier returns an outlet accessory. func NewDehumidifier(info Info) *Dehumidifier { - a := New(info, TypeDehumidifier) + a := Dehumidifier{} + a.A = New(info, TypeDehumidifier) - d := service.NewDehumidifier() - a.Ss = append(a.Ss, d.S) + a.Dehumidifier = service.NewDehumidifier() + a.AddS(a.Dehumidifier.S) - return &Dehumidifier{ - A: a, - Dehumidifier: d, - } + return &a } diff --git a/accessory/door.go b/accessory/door.go index 2b197da..29bce51 100644 --- a/accessory/door.go +++ b/accessory/door.go @@ -6,19 +6,16 @@ import ( type Door struct { *A - Door *service.Door } // NewDoor returns a door accessory. func NewDoor(info Info) *Door { - a := New(info, TypeDoor) + a := Door{} + a.A = New(info, TypeDoor) - door := service.NewDoor() - a.Ss = append(a.Ss, door.S) + a.Door = service.NewDoor() + a.AddS(a.Door.S) - return &Door{ - A: a, - Door: door, - } + return &a } diff --git a/accessory/fan.go b/accessory/fan.go index 8882796..5409d5d 100644 --- a/accessory/fan.go +++ b/accessory/fan.go @@ -6,19 +6,16 @@ import ( type Fan struct { *A - Fan *service.Fan } // NewFan returns a fan accessory. func NewFan(info Info) *Fan { - a := New(info, TypeFan) + a := Fan{} + a.A = New(info, TypeFan) - fan := service.NewFan() - a.Ss = append(a.Ss, fan.S) + a.Fan = service.NewFan() + a.AddS(a.Fan.S) - return &Fan{ - A: a, - Fan: fan, - } + return &a } diff --git a/accessory/faucet.go b/accessory/faucet.go index ef4b405..810c0ab 100644 --- a/accessory/faucet.go +++ b/accessory/faucet.go @@ -6,19 +6,16 @@ import ( type Faucet struct { *A - Faucet *service.Faucet } // NewFaucet returns an outlet accessory. func NewFaucet(info Info) *Faucet { - a := New(info, TypeFaucet) + a := Faucet{} + a.A = New(info, TypeFaucet) - faucet := service.NewFaucet() - a.Ss = append(a.Ss, faucet.S) + a.Faucet = service.NewFaucet() + a.AddS(a.Faucet.S) - return &Faucet{ - A: a, - Faucet: faucet, - } + return &a } diff --git a/accessory/garage_door_opener.go b/accessory/garage_door_opener.go index 121d8e3..ed1c31e 100644 --- a/accessory/garage_door_opener.go +++ b/accessory/garage_door_opener.go @@ -6,19 +6,16 @@ import ( type GarageDoorOpener struct { *A - GarageDoorOpener *service.GarageDoorOpener } // NewGarageDoorOpener returns a garage door opener accessory. func NewGarageDoorOpener(info Info) *GarageDoorOpener { - a := New(info, TypeGarageDoorOpener) + a := GarageDoorOpener{} + a.A = New(info, TypeGarageDoorOpener) - garage := service.NewGarageDoorOpener() - a.Ss = append(a.Ss, garage.S) + a.GarageDoorOpener = service.NewGarageDoorOpener() + a.AddS(a.GarageDoorOpener.S) - return &GarageDoorOpener{ - A: a, - GarageDoorOpener: garage, - } + return &a } diff --git a/accessory/heater.go b/accessory/heater.go index 5446f1c..be0d8aa 100644 --- a/accessory/heater.go +++ b/accessory/heater.go @@ -13,6 +13,7 @@ type Heater struct { func NewHeater(info Info) *Heater { a := Heater{} a.A = New(info, TypeHeater) + a.Heater = service.NewHeater() a.AddS(a.Heater.S) diff --git a/accessory/humidifier.go b/accessory/humidifier.go index cd97cdd..42ffd8e 100644 --- a/accessory/humidifier.go +++ b/accessory/humidifier.go @@ -6,19 +6,16 @@ import ( type Humidifier struct { *A - Humidifier *service.Humidifier } // NewHumidifier returns an outlet accessory. func NewHumidifier(info Info) *Humidifier { - a := New(info, TypeHumidifier) + a := Humidifier{} + a.A = New(info, TypeHumidifier) - h := service.NewHumidifier() - a.Ss = append(a.Ss, h.S) + a.Humidifier = service.NewHumidifier() + a.AddS(a.Humidifier.S) - return &Humidifier{ - A: a, - Humidifier: h, - } + return &a } diff --git a/accessory/lightbulb.go b/accessory/lightbulb.go index 162f182..308a667 100644 --- a/accessory/lightbulb.go +++ b/accessory/lightbulb.go @@ -11,13 +11,11 @@ type Lightbulb struct { // NewLightbulb returns an light bulb accessory. func NewLightbulb(info Info) *Lightbulb { - a := New(info, TypeLightbulb) + a := Lightbulb{} + a.A = New(info, TypeLightbulb) - l := service.NewLightbulb() - a.AddS(l.S) + a.Lightbulb = service.NewLightbulb() + a.AddS(a.Lightbulb.S) - return &Lightbulb{ - A: a, - Lightbulb: l, - } + return &a } diff --git a/accessory/motion_sensor.go b/accessory/motion_sensor.go new file mode 100644 index 0000000..3a78daa --- /dev/null +++ b/accessory/motion_sensor.go @@ -0,0 +1,19 @@ +package accessory + +import "github.com/brutella/hap/service" + +type MotionSensor struct { + *A + MotionSensor *service.MotionSensor +} + +// NewMotionSensor returns a motion sensor. +func NewMotionSensor(info Info) *MotionSensor { + a := MotionSensor{} + a.A = New(info, TypeSensor) + + a.MotionSensor = service.NewMotionSensor() + a.AddS(a.MotionSensor.S) + + return &a +} diff --git a/accessory/outlet.go b/accessory/outlet.go index 08e666c..bb91136 100644 --- a/accessory/outlet.go +++ b/accessory/outlet.go @@ -6,19 +6,16 @@ import ( type Outlet struct { *A - Outlet *service.Outlet } // NewOutlet returns an outlet accessory. func NewOutlet(info Info) *Outlet { - a := New(info, TypeOutlet) + a := Outlet{} + a.A = New(info, TypeOutlet) - outlet := service.NewOutlet() - a.Ss = append(a.Ss, outlet.S) + a.Outlet = service.NewOutlet() + a.AddS(a.Outlet.S) - return &Outlet{ - A: a, - Outlet: outlet, - } + return &a } diff --git a/accessory/security_system.go b/accessory/security_system.go index 4640435..d8f8f7b 100644 --- a/accessory/security_system.go +++ b/accessory/security_system.go @@ -6,19 +6,16 @@ import ( type SecuritySystem struct { *A - SecuritySystem *service.SecuritySystem } // NewSecuritySystem returns a security system accessory. func NewSecuritySystem(info Info) *SecuritySystem { - a := New(info, TypeSecuritySystem) + a := SecuritySystem{} + a.A = New(info, TypeSecuritySystem) - garage := service.NewSecuritySystem() - a.Ss = append(a.Ss, garage.S) + a.SecuritySystem = service.NewSecuritySystem() + a.AddS(a.SecuritySystem.S) - return &SecuritySystem{ - A: a, - SecuritySystem: garage, - } + return &a } diff --git a/accessory/switch.go b/accessory/switch.go index 758b119..8fcfacf 100644 --- a/accessory/switch.go +++ b/accessory/switch.go @@ -13,6 +13,7 @@ type Switch struct { func NewSwitch(info Info) *Switch { a := Switch{} a.A = New(info, TypeSwitch) + a.Switch = service.NewSwitch() a.AddS(a.Switch.S) diff --git a/accessory/television.go b/accessory/television.go index ad10f1a..2a40512 100644 --- a/accessory/television.go +++ b/accessory/television.go @@ -14,10 +14,11 @@ type Television struct { func NewTelevision(info Info) *Television { a := Television{} a.A = New(info, TypeTelevision) - a.Television = service.NewTelevision() - a.Speaker = service.NewSpeaker() + a.Television = service.NewTelevision() a.AddS(a.Television.S) + + a.Speaker = service.NewSpeaker() a.AddS(a.Speaker.S) return &a diff --git a/accessory/thermometer.go b/accessory/thermometer.go index 38d7a26..3f94340 100644 --- a/accessory/thermometer.go +++ b/accessory/thermometer.go @@ -6,7 +6,6 @@ import ( type Thermometer struct { *A - TempSensor *service.TemperatureSensor } @@ -14,8 +13,8 @@ type Thermometer struct { func NewTemperatureSensor(info Info) *Thermometer { a := Thermometer{} a.A = New(info, TypeThermostat) - a.TempSensor = service.NewTemperatureSensor() + a.TempSensor = service.NewTemperatureSensor() a.AddS(a.TempSensor.S) return &a diff --git a/accessory/thermostat.go b/accessory/thermostat.go index edc691b..43c50de 100644 --- a/accessory/thermostat.go +++ b/accessory/thermostat.go @@ -6,7 +6,6 @@ import ( type Thermostat struct { *A - Thermostat *service.Thermostat } @@ -14,8 +13,8 @@ type Thermostat struct { func NewThermostat(info Info) *Thermostat { a := Thermostat{} a.A = New(info, TypeThermostat) - a.Thermostat = service.NewThermostat() + a.Thermostat = service.NewThermostat() a.AddS(a.Thermostat.S) return &a diff --git a/accessory/window.go b/accessory/window.go index 9935344..6e6a0ec 100644 --- a/accessory/window.go +++ b/accessory/window.go @@ -13,6 +13,7 @@ type Window struct { func NewWindow(info Info) *Window { a := Window{} a.A = New(info, TypeWindow) + a.Window = service.NewWindow() a.AddS(a.Window.S) diff --git a/accessory/window_covering.go b/accessory/window_covering.go index 2042f42..f94af60 100644 --- a/accessory/window_covering.go +++ b/accessory/window_covering.go @@ -13,6 +13,7 @@ type WindowCovering struct { func NewWindowCovering(info Info) *WindowCovering { a := WindowCovering{} a.A = New(info, TypeWindowCovering) + a.WindowCovering = service.NewWindowCovering() a.AddS(a.WindowCovering.S) diff --git a/characteristic/activity_interval.go b/characteristic/activity_interval.go new file mode 100644 index 0000000..e306450 --- /dev/null +++ b/characteristic/activity_interval.go @@ -0,0 +1,18 @@ +package characteristic + +const TypeActivityInterval = "23B" + +type ActivityInterval struct { + *Int +} + +func NewActivityInterval() *ActivityInterval { + c := NewInt(TypeActivityInterval) + c.Format = FormatUInt32 + c.Permissions = []string{PermissionRead, PermissionEvents} + c.SetMinValue(0) + c.SetStepValue(1) + c.SetValue(0) + + return &ActivityInterval{c} +} diff --git a/characteristic/bool.go b/characteristic/bool.go index 0c9aebd..cc59a41 100644 --- a/characteristic/bool.go +++ b/characteristic/bool.go @@ -25,19 +25,19 @@ func (c *Bool) SetValue(v bool) { // Value returns the value of c as bool. func (c *Bool) Value() bool { - return c.C.value().(bool) + return c.C.Value().(bool) } // OnSetRemoteValue set c.SetValueRequestFunc and calls fn. // If the function returns an error, the code -70402 is // included in the HTTP response. func (c *Bool) OnSetRemoteValue(fn func(v bool) error) { - c.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { if err := fn(v.(bool)); err != nil { log.Debug.Println(err) - return -70402 + return nil, -70402 } - return 0 + return nil, 0 } } diff --git a/characteristic/bytes.go b/characteristic/bytes.go index 80db518..ae8d1d7 100644 --- a/characteristic/bytes.go +++ b/characteristic/bytes.go @@ -25,7 +25,7 @@ func (c *Bytes) SetValue(v []byte) { // Value returns the value of c as byte array. func (c *Bytes) Value() []byte { - str := c.C.value().(string) + str := c.C.Value().(string) if b, err := base64.StdEncoding.DecodeString(str); err != nil { return []byte{} } else { @@ -37,13 +37,13 @@ func (c *Bytes) Value() []byte { // If the function returns an error, the code -70402 is // included in the HTTP response. func (c *Bytes) OnSetRemoteValue(fn func(v []byte) error) { - c.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { str, _ := base64.StdEncoding.DecodeString(v.(string)) if err := fn(str); err != nil { log.Debug.Println(err) - return -70402 + return nil, -70402 } - return 0 + return nil, 0 } } diff --git a/characteristic/c.go b/characteristic/c.go index 7e24488..7235e47 100644 --- a/characteristic/c.go +++ b/characteristic/c.go @@ -1,6 +1,8 @@ package characteristic import ( + "sync" + "github.com/brutella/hap/log" "github.com/xiam/to" @@ -11,18 +13,20 @@ import ( const ( PermissionRead = "pr" // The characteristic can only be read by paired controllers. PermissionWrite = "pw" // The characteristic can only be written by paired controllers. + PermissionTimedWrite = "tw" // The characteristic allows only timed write procedure. PermissionEvents = "ev" // The characteristic supports events. - PermissionHidden = "hd" // The characteristic is hidden from the user - PermissionWriteResponse = "wr" // The characteristic supports write response + PermissionHidden = "hd" // The characteristic is hidden from the user. + PermissionWriteResponse = "wr" // The characteristic supports write response. ) const ( - UnitPercentage = "percentage" // % - UnitArcDegrees = "arcdegrees" // ° - UnitCelsius = "celsius" // °C - UnitLux = "lux" // lux - UnitSeconds = "seconds" // sec - UnitPPM = "ppm" // ppm + UnitPercentage = "percentage" // % + UnitArcDegrees = "arcdegrees" // ° + UnitCelsius = "celsius" // °C + UnitLux = "lux" // lux + UnitSeconds = "seconds" // sec + UnitPPM = "ppm" // ppm + UnitMicrogramsPerCubicMeter = "micrograms/m^3" ) const ( @@ -82,9 +86,6 @@ type C struct { // ValidRange is a 2 element array the valid range start and end. ValidRange []int - // Stores which connected client has events enabled for this characteristic. - Events map[string]bool - // ValueRequestFunc is called when the value of C is requested by a // paired controller via an HTTP request. // If the value of C represents the state of a remote object, you can use @@ -101,7 +102,7 @@ type C struct { // If the communication fails, you can return a code != 0. // In this case, the server responds with the HTTP status code 500 and the code // in the response body (as defined in HAP-R2 6.7.1.4 HAP Status Codes). - SetValueRequestFunc func(value interface{}, request *http.Request) int + SetValueRequestFunc func(value interface{}, request *http.Request) (response interface{}, code int) // A list of update value functions. // There are called when the value of the characteristic is updated. @@ -111,12 +112,17 @@ type C struct { // when the new value is the same as the old value. // This flag is only used for programmable switch events. updateOnSameValue bool + + // Stores which connected client has events enabled for this characteristic. + events map[string]bool + + m sync.Mutex } // New returns a new characteristic. func New() *C { return &C{ - Events: make(map[string]bool), + events: make(map[string]bool), valUpdateFuncs: make([]ValueUpdateFunc, 0), } } @@ -124,24 +130,26 @@ func New() *C { // OnCValueUpdate register the given function which is called // when the value of the characteristic is updated. func (c *C) OnCValueUpdate(fn ValueUpdateFunc) { + c.m.Lock() c.valUpdateFuncs = append(c.valUpdateFuncs, fn) + c.m.Unlock() } // Sets the value of c to val and returns a status code. // The server invokes this function when the value is updated by an http request. -func (c *C) SetValueRequest(val interface{}, req *http.Request) int { +func (c *C) SetValueRequest(val interface{}, req *http.Request) (interface{}, int) { // check write permission - if !c.IsWritable() { + if req != nil && !c.IsWritable() { log.Info.Printf("writing %v by %s not allowed\n", val, req.RemoteAddr) - return -70404 + return val, -70404 } return c.setValue(val, req) } -func (c *C) setValue(v interface{}, req *http.Request) int { +func (c *C) setValue(v interface{}, req *http.Request) (interface{}, int) { newVal := c.convert(v) - + response := newVal // Value must be within min and max switch c.Format { case FormatFloat: @@ -150,34 +158,44 @@ func (c *C) setValue(v interface{}, req *http.Request) int { newVal = c.clampInt(newVal.(int)) } + c.m.Lock() + // reference old value + oldVal := c.Val + c.m.Unlock() + // ignore the same newVal - if c.Val == newVal && !c.updateOnSameValue { + if oldVal == newVal && !c.updateOnSameValue { // no error - return 0 + return nil, 0 } if !c.validVal(newVal) { - return -70410 + return nil, -70410 } if c.SetValueRequestFunc != nil && req != nil { - if s := c.SetValueRequestFunc(newVal, req); s != 0 { - return s + v, c := c.SetValueRequestFunc(newVal, req) + if c != 0 { + return v, c } - } - // reference old value - oldVal := c.Val + if v != nil { + response = v + } + } + c.m.Lock() // update to new value c.Val = newVal + funcs := c.valUpdateFuncs + c.m.Unlock() // call update funcs - for _, fn := range c.valUpdateFuncs { + for _, fn := range funcs { fn(c, newVal, oldVal, req) } - return 0 + return response, 0 } // ValueRequest returns the value of C and a status code. @@ -194,14 +212,35 @@ func (c *C) ValueRequest(req *http.Request) (interface{}, int) { return c.ValueRequestFunc(req) } - return c.value(), 0 + return c.Value(), 0 } -// value returns the value of C and a status code. -func (c *C) value() interface{} { +// Value returns the value of C +func (c *C) Value() interface{} { + c.m.Lock() + defer c.m.Unlock() return c.Val } +func (c *C) SetEvent(remoteAddr string, enable bool) { + c.m.Lock() + defer c.m.Unlock() + c.events[remoteAddr] = enable +} + +func (c *C) HasEventsEnabled(remoteAddr string) bool { + c.m.Lock() + defer c.m.Unlock() + + ev, ok := c.events[remoteAddr] + if ok { + return ev + } + return false +} + +// IsWritable returns true if clients are allowed +// to update the value of the characteristic. func (c *C) IsWritable() bool { for _, p := range c.Permissions { if p == PermissionWrite { @@ -212,6 +251,8 @@ func (c *C) IsWritable() bool { return false } +// IsReadable returns true if clients are allowed +// to read the value of the characteristic. func (c *C) IsReadable() bool { for _, p := range c.Permissions { if p == PermissionRead { @@ -222,6 +263,32 @@ func (c *C) IsReadable() bool { return false } +// RequiresTimedWrite returns true if the value can +// only be set with a timed write procedure. +func (c *C) RequiresTimedWrite() bool { + for _, p := range c.Permissions { + if p == PermissionTimedWrite { + return true + } + } + + return false +} + +// IsWriteResponse returns true if the value can +// return a response on write +func (c *C) IsWriteResponse() bool { + for _, p := range c.Permissions { + if p == PermissionWriteResponse { + return true + } + } + + return false +} + +// IsObservable returns true if clients are allowed +// to observe the value of the characteristic. func (c *C) IsObservable() bool { for _, p := range c.Permissions { if p == PermissionEvents { @@ -232,6 +299,8 @@ func (c *C) IsObservable() bool { return false } +// IsObservable returns true if the value of the +// characteristic can only be updated, but not read. func (c *C) IsWriteOnly() bool { return len(c.Permissions) == 1 && c.Permissions[0] == PermissionWrite } @@ -241,12 +310,11 @@ func (c *C) MarshalJSON() ([]byte, error) { Id uint64 `json:"iid"` // managed by accessory Type string `json:"type"` Permissions []string `json:"perms"` - Description string `json:"description,omitempty"` // manufacturer description (optional) - - Value interface{} `json:"value,omitempty"` // nil for write-only characteristics - Format string `json:"format"` - Unit string `json:"unit,omitempty"` + Format string `json:"format"` + Value *V `json:"value,omitempty"` + Description string `json:"description,omitempty"` // manufacturer description (optional) + Unit string `json:"unit,omitempty"` MaxLen int `json:"maxLen,omitempty"` MaxValue interface{} `json:"maxValue,omitempty"` MinValue interface{} `json:"minValue,omitempty"` @@ -268,13 +336,28 @@ func (c *C) MarshalJSON() ([]byte, error) { ValidRange: c.ValidRange, } + // If the characteristic is readable, the value + // must be present in the json representation. if c.IsReadable() { - d.Value = c.value() + // 2022-03-21 (mah) FIXME provide a http request instead of nil + if v, s := c.ValueRequest(nil); s == 0 { + d.Value = &V{v} + } else { + d.Value = &V{c.Value()} // dummy "zero" value + } } return json.Marshal(&d) } +type V struct { + Value interface{} +} + +func (v V) MarshalJSON() ([]byte, error) { + return json.Marshal(v.Value) +} + func (c *C) clampFloat(value float64) interface{} { min, minOK := c.MinVal.(float64) max, maxOK := c.MaxVal.(float64) @@ -315,6 +398,11 @@ func (c *C) convert(v interface{}) interface{} { } func (c *C) validVal(v interface{}) bool { + iv, ok := v.(int) + if !ok { + return true + } + if len(c.ValidVals) > 0 { for _, val := range c.ValidVals { if val == v { @@ -325,7 +413,7 @@ func (c *C) validVal(v interface{}) bool { return false } - if iv, ok := v.(int); ok && len(c.ValidRange) == 2 { + if len(c.ValidRange) == 2 { return c.ValidRange[0] <= iv && c.ValidRange[1] >= iv } diff --git a/characteristic/c_test.go b/characteristic/c_test.go index 6363962..3a33c36 100644 --- a/characteristic/c_test.go +++ b/characteristic/c_test.go @@ -1,7 +1,9 @@ package characteristic import ( + "encoding/json" "net/http" + "reflect" "testing" ) @@ -118,21 +120,26 @@ func TestReadOnly(t *testing.T) { if is, want := c.Value(), "Matthias"; is != want { t.Fatalf("is=%v want=%v", is, want) } + + c.SetValueRequest("Gottfried", nil) + if is, want := c.Value(), "Gottfried"; is != want { + t.Fatalf("is=%v want=%v", is, want) + } } -func TestSetValueRequestFunc(t *testing.T) { +func TestSetValueRequestFuncError(t *testing.T) { c := NewBrightness() c.SetValue(100) - c.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (response interface{}, status int) { if r != nil { - return -70408 + status = -70408 } - return 0 + return } - s := c.SetValueRequest(50, &http.Request{}) + _, s := c.SetValueRequest(50, &http.Request{}) if is, want := s, -70408; is != want { t.Fatalf("%v != %v", is, want) } @@ -142,6 +149,24 @@ func TestSetValueRequestFunc(t *testing.T) { } } +func TestOnSetRemoteValue(t *testing.T) { + c := NewBrightness() + + c.SetValue(100) + c.OnSetRemoteValue(func(v int) error { + return nil + }) + + v, s := c.SetValueRequest(50, &http.Request{}) + if is, want := s, 0; is != want { + t.Fatalf("%v != %v", is, want) + } + + if is, want := v, 50; is != want { + t.Fatalf("is=%v want=%v", is, want) + } +} + func TestValidValues(t *testing.T) { c := NewTargetHeaterCoolerState() c.ValidVals = []int{TargetHeaterCoolerStateAuto, TargetHeaterCoolerStateHeat} @@ -154,6 +179,7 @@ func TestValidValues(t *testing.T) { t.Fatal("no error expected") } } + func TestValidRange(t *testing.T) { c := NewTargetHeaterCoolerState() c.ValidRange = []int{TargetHeaterCoolerStateAuto, TargetHeaterCoolerStateHeat} @@ -166,3 +192,66 @@ func TestValidRange(t *testing.T) { t.Fatal("no error expected") } } + +func encodeDecodeJson(c *C, t *testing.T) map[string]interface{} { + j, err := c.MarshalJSON() + if err != nil { + t.Fatal("cannot MarshalJSON: ", err) + } + + var jsonMap map[string]interface{} + err = json.Unmarshal(j, &jsonMap) + if err != nil { + t.Fatal("invalid encoded JSON: ", err) + } + + return jsonMap +} + +func TestCharacteristicJson(t *testing.T) { + cs := []*C{ + //NewContactSensorState().C, // int + NewCurrentTemperature().C, // float + NewCurrentTransport().C, // bool + NewAccessoryIdentifier().C, // string + } + + for _, c := range cs { + jsonMap := encodeDecodeJson(c, t) + + // verify properties + if is, want := jsonMap["type"], c.Type; is != want { + t.Fatalf("marshaled type is wrong: is=%v wanted=%v", is, want) + } + if is, want := jsonMap["format"], c.Format; is != want { + t.Fatalf("marshaled format is wrong: is=%v wanted=%v", is, want) + } + if is, want := jsonMap["value"], c.Val; is != want { + t.Fatalf("marshaled value is wrong: is=%v wanted=%v", is, want) + } + + // set a ValueRequestFunc that returns an error + c.ValueRequestFunc = func(r *http.Request) (response interface{}, status int) { + return nil, -70408 + } + + // re-encode + jsonMap = encodeDecodeJson(c, t) + + jv, exists := jsonMap["value"] + if !exists { + t.Fatalf("errored characteristic is missing \"value\": %+v", jsonMap) + } + if is, want := reflect.TypeOf(jv), reflect.TypeOf(c.Val); is != want { + t.Fatalf("json-encoded value is of wrong type: is=%v want=%v", is, want) + } + } + + // special case /identify must not emit any "value" + id := NewIdentify().C + jsonMap := encodeDecodeJson(id, t) + + if _, exists := jsonMap["value"]; exists { + t.Fatalf("Identify characteristic cannot emit \"value\": %+v", jsonMap) + } +} diff --git a/characteristic/float.go b/characteristic/float.go index 22c5ea6..beaacbf 100644 --- a/characteristic/float.go +++ b/characteristic/float.go @@ -35,7 +35,7 @@ func (c *Float) SetStepValue(v float64) { // Value returns the value of c as float64. func (c *Float) Value() float64 { - return c.C.value().(float64) + return c.C.Value().(float64) } func (c *Float) MinValue() float64 { @@ -54,12 +54,12 @@ func (c *Float) StepValue() float64 { // If the function returns an error, the code -70402 is // included in the HTTP response. func (c *Float) OnSetRemoteValue(fn func(v float64) error) { - c.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { if err := fn(v.(float64)); err != nil { log.Debug.Println(err) - return -70402 + return nil, -70402 } - return 0 + return nil, 0 } } diff --git a/characteristic/heart_beat.go b/characteristic/heart_beat.go new file mode 100644 index 0000000..bb45388 --- /dev/null +++ b/characteristic/heart_beat.go @@ -0,0 +1,16 @@ +package characteristic + +const TypeHeartBeat = "24A" + +type HeartBeat struct { + *Int +} + +func NewHeartBeat() *HeartBeat { + c := NewInt(TypeHeartBeat) + c.Format = FormatUInt32 + c.Permissions = []string{PermissionRead, PermissionEvents} + c.SetValue(0) + + return &HeartBeat{c} +} diff --git a/characteristic/hold_position.go b/characteristic/hold_position.go index 967175a..387c2b0 100644 --- a/characteristic/hold_position.go +++ b/characteristic/hold_position.go @@ -13,5 +13,7 @@ func NewHoldPosition() *HoldPosition { c.Format = FormatBool c.Permissions = []string{PermissionWrite} + c.updateOnSameValue = true + return &HoldPosition{c} } diff --git a/characteristic/int.go b/characteristic/int.go index 509e064..5bc7e1d 100644 --- a/characteristic/int.go +++ b/characteristic/int.go @@ -19,7 +19,7 @@ func NewInt(t string) *Int { // SetValue sets a value func (c *Int) SetValue(v int) error { - code := c.setValue(v, nil) + _, code := c.setValue(v, nil) switch code { case -70410: return fmt.Errorf("invalid value %d", v) @@ -44,7 +44,7 @@ func (c *Int) SetStepValue(v int) { // Value returns the value of c as integer. func (c *Int) Value() int { - return c.C.value().(int) + return c.C.Value().(int) } func (c *Int) MinValue() int { @@ -63,12 +63,12 @@ func (c *Int) StepValue() int { // If the function returns an error, the code -70402 is // included in the HTTP response. func (c *Int) OnSetRemoteValue(fn func(v int) error) { - c.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { if err := fn(v.(int)); err != nil { log.Debug.Println(err) - return -70402 + return nil, -70402 } - return 0 + return nil, 0 } } diff --git a/characteristic/ping.go b/characteristic/ping.go new file mode 100644 index 0000000..2a4d0b0 --- /dev/null +++ b/characteristic/ping.go @@ -0,0 +1,17 @@ +package characteristic + +const TypePing = "23C" + +type Ping struct { + *Bytes +} + +func NewPing() *Ping { + c := NewBytes(TypePing) + c.Format = FormatTLV8 + c.Permissions = []string{PermissionRead} + + c.SetValue([]byte{}) + + return &Ping{c} +} diff --git a/characteristic/programmable_switch_event.go b/characteristic/programmable_switch_event.go index caade19..aec07ae 100644 --- a/characteristic/programmable_switch_event.go +++ b/characteristic/programmable_switch_event.go @@ -1,6 +1,8 @@ package characteristic -// THIS FILE IS AUTO-GENERATED +import ( + "net/http" +) const ( ProgrammableSwitchEventSinglePress int = 0 @@ -18,8 +20,13 @@ func NewProgrammableSwitchEvent() *ProgrammableSwitchEvent { c := NewInt(TypeProgrammableSwitchEvent) c.Format = FormatUInt8 c.Permissions = []string{PermissionRead, PermissionEvents} - c.SetValue(0) + + // always return nil (HAP 9.75) + c.ValueRequestFunc = func(*http.Request) (interface{}, int) { + return nil, 0 + } + c.updateOnSameValue = true return &ProgrammableSwitchEvent{c} diff --git a/characteristic/sleep_interval.go b/characteristic/sleep_interval.go new file mode 100644 index 0000000..df6fc9b --- /dev/null +++ b/characteristic/sleep_interval.go @@ -0,0 +1,19 @@ +package characteristic + +const TypeSleepInterval = "23A" + +type SleepInterval struct { + *Int +} + +func NewSleepInterval() *SleepInterval { + c := NewInt(TypeSleepInterval) + c.Format = FormatUInt32 + c.Permissions = []string{PermissionRead} + c.SetMinValue(0) + c.SetMaxValue(67108863) + c.SetStepValue(1) + c.SetValue(0) + + return &SleepInterval{c} +} diff --git a/characteristic/string.go b/characteristic/string.go index bb0b1b5..7f41b52 100644 --- a/characteristic/string.go +++ b/characteristic/string.go @@ -25,19 +25,19 @@ func (c *String) SetValue(v string) { // Value returns the value of c as string. func (c *String) Value() string { - return c.C.value().(string) + return c.C.Value().(string) } // OnSetRemoteValue set c.SetValueRequestFunc and calls fn. // If the function returns an error, the code -70402 is // included in the HTTP response. func (c *String) OnSetRemoteValue(fn func(v string) error) { - c.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { if err := fn(v.(string)); err != nil { log.Debug.Println(err) - return -70402 + return nil, -70402 } - return 0 + return nil, 0 } } diff --git a/characteristics.go b/characteristics.go index 27bd1a7..b59124e 100644 --- a/characteristics.go +++ b/characteristics.go @@ -1,6 +1,8 @@ package hap import ( + "time" + "github.com/brutella/hap/accessory" "github.com/brutella/hap/characteristic" "github.com/brutella/hap/log" @@ -12,9 +14,9 @@ import ( ) type characteristicData struct { - Aid uint64 `json:"aid"` - Iid uint64 `json:"iid"` - Value interface{} `json:"value,omitempty"` + Aid uint64 `json:"aid"` + Iid uint64 `json:"iid"` + Value *characteristic.V `json:"value,omitempty"` // optional values Type *string `json:"type,omitempty"` @@ -29,6 +31,15 @@ type characteristicData struct { MaxLen *int `json:"maxLen,omitempty"` ValidValues []int `json:"valid-values,omitempty"` ValidRange []int `json:"valid-values-range,omitempty"` +} + +type putCharacteristicData struct { + Aid uint64 `json:"aid"` + Iid uint64 `json:"iid"` + + Value interface{} `json:"value,omitempty"` + Status *int `json:"status,omitempty"` + Events *bool `json:"ev,omitempty"` Remote *bool `json:"remote,omitempty"` Response *bool `json:"r,omitempty"` @@ -79,7 +90,7 @@ func (srv *Server) getCharacteristics(res http.ResponseWriter, req *http.Request err = true cdata.Status = &s } else { - cdata.Value = v + cdata.Value = &characteristic.V{v} } if meta { @@ -110,10 +121,7 @@ func (srv *Server) getCharacteristics(res http.ResponseWriter, req *http.Request // Should the response include the events flag? if ev { - var ev bool - if v, ok := c.Events[req.RemoteAddr]; ok { - ev = v - } + ev := c.HasEventsEnabled(req.RemoteAddr) cdata.Events = &ev } @@ -133,6 +141,14 @@ func (srv *Server) getCharacteristics(res http.ResponseWriter, req *http.Request log.Debug.Println(toJSON(resp)) if err { + // when there's an error somewhere, "status: 0" must now be explicit + noError := 0 + for _, c := range arr { + if c.Status == nil { + c.Status = &noError + } + } + JsonMultiStatus(res, resp) } else { JsonOK(res, resp) @@ -147,20 +163,24 @@ func (srv *Server) putCharacteristics(res http.ResponseWriter, req *http.Request } data := struct { - Cs []characteristicData `json:"characteristics"` + Cs []putCharacteristicData `json:"characteristics"` + Pid uint64 `json:"pid"` }{} + err := json.NewDecoder(req.Body).Decode(&data) if err != nil { JsonError(res, JsonStatusInvalidValueInRequest) return } + timedWr := srv.TimedWrite(req) log.Debug.Println(toJSON(data)) - arr := []*characteristicData{} + arr := []*putCharacteristicData{} for _, d := range data.Cs { c := srv.findC(d.Aid, d.Iid) - cdata := &characteristicData{ + + cdata := &putCharacteristicData{ Aid: d.Aid, Iid: d.Iid, } @@ -172,18 +192,38 @@ func (srv *Server) putCharacteristics(res http.ResponseWriter, req *http.Request continue } - if d.Value != nil { - s := c.SetValueRequest(d.Value, req) - if s != 0 { - cdata.Status = &s + var value interface{} + var status int + if c.RequiresTimedWrite() { + if time.Now().After(timedWr.deadline) { + // HAP 6.7.2.4 + // If the accessory receives an Execute Write Request after the TTL has expired it must ignore + // the request and respond with HAP status error code -70410 (HAPIPStatusErrorCodeInvalidWrite). + log.Info.Println("timed write wall time exceeded") + status = -70410 + } + if data.Pid != timedWr.pid { + // HAP 6.7.2.4 + // If the accessory receives a standard write request on a characteristic which requires timed write, + // the accessory must respond with HAP status error code -70410 (HAPIPStatusErrorCodeInvalidWrite). + log.Info.Println("timed write transaction id invalid") + status = -70410 } } - if d.Response != nil { - if v, s := c.ValueRequest(req); s != 0 { - cdata.Status = &s - } else { - cdata.Value = v + if d.Value != nil && status == 0 { + value, status = c.SetValueRequest(d.Value, req) + } + + if status != 0 { + cdata.Status = &status + } + + if (d.Response != nil || c.IsWriteResponse()) && value != nil { + cdata.Value = value + + if c.IsWriteResponse() { + cdata.Status = &status } } @@ -193,7 +233,7 @@ func (srv *Server) putCharacteristics(res http.ResponseWriter, req *http.Request cdata.Status = &status arr = append(arr, cdata) } else { - c.Events[req.RemoteAddr] = *d.Events + c.SetEvent(req.RemoteAddr, *d.Events) } } @@ -202,13 +242,15 @@ func (srv *Server) putCharacteristics(res http.ResponseWriter, req *http.Request } } + srv.DelTimedWrite(req) + if len(arr) == 0 { res.WriteHeader(http.StatusNoContent) return } resp := struct { - Characteristics []*characteristicData `json:"characteristics"` + Characteristics []*putCharacteristicData `json:"characteristics"` }{arr} log.Debug.Println(toJSON(resp)) @@ -234,3 +276,30 @@ func (srv *Server) findC(aid, iid uint64) *characteristic.C { return nil } + +func (srv *Server) prepareCharacteristics(res http.ResponseWriter, req *http.Request) { + if !srv.IsAuthorized(req) { + log.Info.Printf("request from %s not authorized\n", req.RemoteAddr) + JsonError(res, JsonStatusInsufficientPrivileges) + return + } + + data := struct { + Ttl uint64 `json:"ttl"` + Pid uint64 `json:"pid"` + }{} + + err := json.NewDecoder(req.Body).Decode(&data) + if err != nil || data.Ttl == 0 || data.Pid == 0 { + JsonError(res, JsonStatusInvalidValueInRequest) + return + } + + srv.SetTimedWrite(data.Ttl, data.Pid, req) + + resp := struct { + Status int `json:"status"` + }{0} + log.Debug.Println(toJSON(resp)) + JsonOK(res, resp) +} diff --git a/conn.go b/conn.go index 22ecca9..603401b 100644 --- a/conn.go +++ b/conn.go @@ -1,6 +1,8 @@ package hap import ( + "sync" + "github.com/brutella/hap/log" "bufio" @@ -20,14 +22,24 @@ type conn struct { // 2022-02-17 (mah) This workaround is needed because switching to encryption is done // after sending a response. But Write() on http.ResponseWriter is not immediate. // So therefore we wait until the next read. - s *session - ss *session + s *session + smu sync.Mutex + ss *session readBuf io.Reader } +func newConn(c net.Conn) *conn { + return &conn{ + Conn: c, + smu: sync.Mutex{}, + } +} + func (c *conn) Upgrade(s *session) { + c.smu.Lock() c.s = s + c.smu.Unlock() } // Write writes bytes to the connection. @@ -48,9 +60,15 @@ func (c *conn) Write(b []byte) (int, error) { } encB, err := ioutil.ReadAll(enc) - n, err := c.Conn.Write(encB) + if err != nil { + return 0, err + } + _, err = c.Conn.Write(encB) + if err != nil { + return 0, err + } - return n, err + return len(b), nil } const ( @@ -60,10 +78,12 @@ const ( // Read reads bytes from the connection. // The read bytes are decrypted when possible. func (c *conn) Read(b []byte) (int, error) { + c.smu.Lock() if c.s != nil { c.ss = c.s c.s = nil } + c.smu.Unlock() if c.ss == nil { return c.Conn.Read(b) diff --git a/fs.go b/fs.go index 95f25ae..441ceab 100644 --- a/fs.go +++ b/fs.go @@ -3,7 +3,6 @@ package hap import ( "github.com/brutella/hap/log" - "bytes" "encoding/hex" "encoding/json" "io/ioutil" @@ -20,7 +19,7 @@ func NewFsStore(dir string) Store { // Prepare filesystem directory // Ensure that execute permission bit is set on all created dirs // Read http://unix.stackexchange.com/questions/21251/why-do-directories-need-the-executable-x-permission-to-be-opened - err := os.MkdirAll(dir, 0755) + err := os.MkdirAll(dir, 0750) if err != nil { log.Info.Panic(err) } @@ -29,37 +28,11 @@ func NewFsStore(dir string) Store { } func (fs *fsStore) Set(key string, value []byte) error { - file, err := os.OpenFile(fs.filePathToFile(key), os.O_WRONLY|os.O_CREATE, 0666) - if err != nil { - return err - } - - defer file.Close() - - _, err = file.Write(value) - return err + return os.WriteFile(fs.filePathToFile(key), value, 0640) } func (fs *fsStore) Get(key string) ([]byte, error) { - file, err := os.OpenFile(fs.filePathToFile(key), os.O_RDONLY, 0666) - if err != nil { - return nil, err - } - - defer file.Close() - - var b bytes.Buffer - var buffer = make([]byte, 32) - for { - n, _ := file.Read(buffer) - if n > 0 { - b.Write(buffer[:n]) - } else { - break - } - } - - return b.Bytes(), nil + return os.ReadFile(fs.filePathToFile(key)) } // Delete removes the file for the corresponding key. diff --git a/go.mod b/go.mod index 1a36c02..46c20b8 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/brutella/hap go 1.16 require ( - github.com/brutella/dnssd v1.2.1 + github.com/brutella/dnssd v1.2.14 github.com/go-chi/chi v1.5.4 github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 - golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 - golang.org/x/text v0.3.7 + golang.org/x/crypto v0.24.0 + gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 ) diff --git a/go.sum b/go.sum index bef6f03..b0422a1 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,110 @@ -github.com/brutella/dnssd v1.2.1 h1:1xG+5itx/SDEP6ukYfAcBnox5WACTNvxZ+SMkAmSrFU= -github.com/brutella/dnssd v1.2.1/go.mod h1:FpJqlQ8+XU6w1vbnG1zJiQPTRE5fvQIRdrcBojMVuuQ= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/brutella/dnssd v1.2.14 h1:qLpTnRTm5peo2jA30hqMIbCuWn8x3sFg3e9o9ODOobw= +github.com/brutella/dnssd v1.2.14/go.mod h1:tG4GE8orv6+irE5rdsNgb6MJSxm6cyMUKdC5jmD22gk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= -github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o= -github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= +github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s= github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3 h1:rz88vn1OH2B9kKorR+QCrcuw6WbizVwahU2Y9Q09xqU= +gopkg.in/Regis24GmbH/go-diacritics.v2 v2.0.3/go.mod h1:vJmfdx2L0+30M90zUd0GCjLV14Ip3ZgWR5+MV1qljOo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/identify.go b/identify.go index 8011722..0465e9f 100644 --- a/identify.go +++ b/identify.go @@ -7,7 +7,7 @@ import ( ) func (srv *Server) identify(res http.ResponseWriter, req *http.Request) { - if srv.isPaired() { + if srv.IsPaired() { log.Info.Printf("request only valid if unpaired") JsonError(res, JsonStatusInsufficientPrivileges) return diff --git a/listener.go b/listener.go index 2545275..3a4e4af 100644 --- a/listener.go +++ b/listener.go @@ -14,7 +14,12 @@ func (ln *listener) Accept() (con net.Conn, err error) { return } - conn := &conn{Conn: con} + // disable TCP keepalives + if tcpconn, ok := con.(*net.TCPConn); ok { + tcpconn.SetKeepAlive(false) + } + + conn := newConn(con) setConn(conn.RemoteAddr().String(), conn) return conn, err diff --git a/notification.go b/notification.go index 2ce0aec..349ee54 100644 --- a/notification.go +++ b/notification.go @@ -20,7 +20,7 @@ func sendNotification(a *accessory.A, c *characteristic.C, req *http.Request) er characteristicData{ Aid: a.Id, Iid: c.Id, - Value: c.Val, + Value: &characteristic.V{c.Val}, }, }, } @@ -62,7 +62,7 @@ func sendNotification(a *accessory.A, c *characteristic.C, req *http.Request) er } // Check which connection has events enabled. - if ev, ok := c.Events[conn.RemoteAddr().String()]; ok && ev { + if c.HasEventsEnabled(conn.RemoteAddr().String()) { log.Debug.Printf("send event to %s:\n%s\n", conn.RemoteAddr(), string(b)) conn.Write(b) } diff --git a/pair-setup.go b/pair-setup.go index c834570..f232f0e 100644 --- a/pair-setup.go +++ b/pair-setup.go @@ -20,32 +20,32 @@ const ( ) type pairSetupPayload struct { - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - Salt []byte `tlv8:"2"` - PublicKey []byte `tlv8:"3"` - Proof []byte `tlv8:"4"` - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - RetryDelay byte `tlv8:"8"` - Certificate []byte `tlv8:"9"` - Signature []byte `tlv8:"10"` - Permissions byte `tlv8:"11"` - FragmentData []byte `tlv8:"13"` - FragmentLast []byte `tlv8:"14"` + Method byte `tlv8:"0,optional"` + Identifier string `tlv8:"1,optional"` + Salt []byte `tlv8:"2,optional"` + PublicKey []byte `tlv8:"3,optional"` + Proof []byte `tlv8:"4,optional"` + EncryptedData []byte `tlv8:"5,optional"` + State byte `tlv8:"6,optional"` + Error byte `tlv8:"7,optional"` + RetryDelay byte `tlv8:"8,optional"` + Certificate []byte `tlv8:"9,optional"` + Signature []byte `tlv8:"10,optional"` + Permissions byte `tlv8:"11,optional"` + FragmentData []byte `tlv8:"13,optional"` + FragmentLast []byte `tlv8:"14,optional"` } func (srv *Server) pairSetup(res http.ResponseWriter, req *http.Request) { // pairing is only allowed if the accessory is not paired yet - if srv.isPaired() { + if srv.IsPaired() { log.Info.Println("pairing is not allowed") tlv8Error(res, M2, TlvErrorUnavailable) return } // pair-setup can only be run by one controller simultaneously - for addr, _ := range sessions() { + for addr, _ := range srv.sessions() { if addr != req.RemoteAddr { log.Info.Printf("simulatenous pairings are not allowed") tlv8Error(res, M2, TlvErrorBusy) @@ -115,7 +115,7 @@ func (srv *Server) pairSetupM1(res http.ResponseWriter, req *http.Request, data tlv8Error(res, M2, TlvErrorUnknown) return } - setSession(req.RemoteAddr, ss) + srv.setSession(req.RemoteAddr, ss) resp := pairSetupM2Payload{ Salt: ss.Salt, @@ -126,7 +126,7 @@ func (srv *Server) pairSetupM1(res http.ResponseWriter, req *http.Request, data } func (srv *Server) pairSetupM3(res http.ResponseWriter, req *http.Request, data pairSetupPayload) { - ses, err := getPairSetupSession(req.RemoteAddr) + ses, err := srv.getPairSetupSession(req.RemoteAddr) if err != nil { log.Info.Println(err) res.WriteHeader(http.StatusInternalServerError) @@ -162,7 +162,7 @@ func (srv *Server) pairSetupM3(res http.ResponseWriter, req *http.Request, data } func (srv *Server) pairSetupM5(res http.ResponseWriter, req *http.Request, data pairSetupPayload) { - ses, err := getPairSetupSession(req.RemoteAddr) + ses, err := srv.getPairSetupSession(req.RemoteAddr) if err != nil { log.Info.Println(err) res.WriteHeader(http.StatusInternalServerError) diff --git a/pair-verify.go b/pair-verify.go index ff0442b..4451d82 100644 --- a/pair-verify.go +++ b/pair-verify.go @@ -12,12 +12,12 @@ import ( ) type pairVerifyPayload struct { - Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - EncryptedData []byte `tlv8:"5"` - State byte `tlv8:"6"` - Signature []byte `tlv8:"10"` + Method byte `tlv8:"0,optional"` + Identifier string `tlv8:"1,optional"` + PublicKey []byte `tlv8:"3,optional"` + EncryptedData []byte `tlv8:"5,optional"` + State byte `tlv8:"6,optional"` + Signature []byte `tlv8:"10,optional"` } type pairVerifySession struct { @@ -118,12 +118,12 @@ func (srv *Server) pairVerifyM1(res http.ResponseWriter, req *http.Request, data SharedKey: sharedKey, EncryptionKey: encKey, } - setSession(req.RemoteAddr, ses) + srv.setSession(req.RemoteAddr, ses) } func (srv *Server) pairVerifyM3(res http.ResponseWriter, req *http.Request, data pairVerifyPayload) { // Get the session for the request. - ses, err := getPairVerifySession(req.RemoteAddr) + ses, err := srv.getPairVerifySession(req.RemoteAddr) if err != nil { log.Info.Println(err) res.WriteHeader(http.StatusInternalServerError) @@ -182,7 +182,7 @@ func (srv *Server) pairVerifyM3(res http.ResponseWriter, req *http.Request, data } // Store the session for the request. - setSession(req.RemoteAddr, ss) + srv.setSession(req.RemoteAddr, ss) conn := getConn(req) if conn == nil { diff --git a/pairings.go b/pairings.go index 2200a04..faae9c7 100644 --- a/pairings.go +++ b/pairings.go @@ -21,7 +21,7 @@ func (srv *Server) pairings(res http.ResponseWriter, req *http.Request) { return } - ss, err := getSession(req.RemoteAddr) + ss, err := srv.getSession(req.RemoteAddr) if err != nil { log.Info.Println(err) res.WriteHeader(http.StatusInternalServerError) @@ -31,9 +31,9 @@ func (srv *Server) pairings(res http.ResponseWriter, req *http.Request) { d := struct { Method byte `tlv8:"0"` - Identifier string `tlv8:"1"` - PublicKey []byte `tlv8:"3"` - Permission byte `tlv8:"11"` + Identifier string `tlv8:"1,optional"` + PublicKey []byte `tlv8:"3,optional"` + Permission byte `tlv8:"11,optional"` State byte `tlv8:"6"` }{} @@ -114,19 +114,19 @@ func (srv *Server) pairings(res http.ResponseWriter, req *http.Request) { } tlv8OK(res, resp) - // Close all connections if no - // admin controller is paired anymore + // If no admin controller is paired anymore, + // close all connections and delete all pairings if !srv.pairedWithAdmin() { for addr, conn := range conns() { log.Debug.Println("Closing connection to", addr) conn.Close() } - return + srv.deleteAllPairings() } // Close connection of deleted controller for addr, conn := range conns() { - ss, err := getSession(addr) + ss, err := srv.getSession(addr) if err != nil { log.Debug.Println("no session for", addr, err) continue diff --git a/pairings_test.go b/pairings_test.go new file mode 100644 index 0000000..0ed98b6 --- /dev/null +++ b/pairings_test.go @@ -0,0 +1,59 @@ +package hap + +import ( + "github.com/brutella/hap/accessory" + + "bytes" + "encoding/hex" + "net/http" + "net/http/httptest" + "testing" +) + +// TestPairingsHandlerRequests is a regression test for the recurring +// "pairings.go: tlv8: EOF" reports (#21, #44). +// +// A ListPairings request carries only Method (tag 0) and State (tag 6) — it has +// no Identifier (tag 1). The handler decoded into a struct that marked +// Identifier as required, so tlv8.UnmarshalReader returned io.EOF and the +// handler rejected the request with HTTP 400. iOS uses ListPairings to +// reconcile a home's controllers (e.g. resident HomePods/Apple TVs), so the +// persistent error left it retrying forever and unable to converge. +// +// The bodies below are captured verbatim from a real iOS controller. +func TestPairingsHandlerRequests(t *testing.T) { + a := accessory.New(accessory.Info{Name: "Test"}, accessory.TypeOutlet) + srv, err := NewServer(NewMemStore(), a) + if err != nil { + t.Fatal(err) + } + + const addr = "192.0.2.10:50000" + srv.mux.Lock() + srv.sess[addr] = &session{Pairing: Pairing{Name: "admin", Permission: PermissionAdmin}} + srv.mux.Unlock() + + cases := []struct { + name string + body string // hex, captured off the wire + }{ + {"ListPairings", "000105060101"}, + {"AddPairing", "000103060101012437463141374344452d454645442d343943342d394431462d39373245364645384544413203208d8b2b72ff96811a7e4ab6ef5e3719bcacc5582b293d5d2ea3fc74dbf13a68620b0101"}, + } + + for _, tc := range cases { + raw, err := hex.DecodeString(tc.body) + if err != nil { + t.Fatal(err) + } + req := httptest.NewRequest(http.MethodPost, "/pairings", bytes.NewReader(raw)) + req.RemoteAddr = addr + rec := httptest.NewRecorder() + + srv.pairings(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("%s: handler returned HTTP %d, want 200 (request rejected — tlv8 unmarshal EOF on a valid request)", tc.name, rec.Code) + } + } +} diff --git a/rtp/setup_endpoints_test.go b/rtp/setup_endpoints_test.go index be23613..9e5dfbc 100644 --- a/rtp/setup_endpoints_test.go +++ b/rtp/setup_endpoints_test.go @@ -1,9 +1,10 @@ package rtp import ( + "testing" + "github.com/brutella/hap/characteristic" "github.com/brutella/hap/tlv8" - "testing" ) func TestSetupEndpoints(t *testing.T) { diff --git a/rtp/stream_configuration.go b/rtp/stream_configuration.go index f0882df..e3e7bd5 100644 --- a/rtp/stream_configuration.go +++ b/rtp/stream_configuration.go @@ -35,9 +35,9 @@ type AudioParameters struct { type RTPParams struct { PayloadType uint8 `tlv8:"1"` - Ssrc int32 `tlv8:"2"` + Ssrc uint32 `tlv8:"2"` Bitrate uint16 `tlv8:"3"` - Interval float32 `tlv8:"4"` // MinimumRTCP interval - ComfortNoisePayloadType uint8 `tlv8:"5"` // only for audio - MTU uint16 `tlv8:"6"` // only for video + Interval float32 `tlv8:"4"` // MinimumRTCP interval + ComfortNoisePayloadType uint8 `tlv8:"5,optional"` // only for audio + MTU uint16 `tlv8:"6,optional"` // only for video } diff --git a/rtp/stream_configuration_test.go b/rtp/stream_configuration_test.go index bf76251..a4af557 100644 --- a/rtp/stream_configuration_test.go +++ b/rtp/stream_configuration_test.go @@ -2,9 +2,10 @@ package rtp import ( "fmt" + "testing" + "github.com/brutella/hap/characteristic" "github.com/brutella/hap/tlv8" - "testing" ) func TestSelectedStreamConfiguration(t *testing.T) { @@ -21,3 +22,74 @@ func TestSelectedStreamConfiguration(t *testing.T) { fmt.Printf("%+v", cfg) } + +func TestRTPParams(t *testing.T) { + + videoBuf := []byte{ + // tag, len, data + 1, 1, 99, + 2, 4, 26, 144, 146, 159, + 3, 2, 43, 1, + 4, 4, 0, 0, 0, 63, + 5, 2, 98, 5, + } + + var videoParam RTPParams + if err := tlv8.Unmarshal(videoBuf, &videoParam); err != nil { + t.Error(err) + } + + if videoParam.PayloadType != 99 { + t.Error("Video PayloadType wrong:", videoParam.PayloadType) + } + + if videoParam.Ssrc != 2677182490 { + t.Error("Video Ssrc wrong", videoParam.Ssrc) + } + + if videoParam.Bitrate != 299 { + t.Error("Video Bitrate wrong", videoParam.Bitrate) + } + + if videoParam.Interval != 0.5 { + t.Error("Video Interval wrong", videoParam.Interval) + } + + if videoParam.ComfortNoisePayloadType != 98 { + t.Error("Video ComfortNoisePayloadType wrong", videoParam.ComfortNoisePayloadType) + } + + audioBuf := []byte{ + // tag, len, data + 1, 1, 110, + 2, 4, 207, 83, 180, 9, + 3, 2, 24, 0, + 4, 4, 0, 0, 160, 64, + 6, 1, 13, + } + + var audioParam RTPParams + if err := tlv8.Unmarshal(audioBuf, &audioParam); err != nil { + t.Error(err) + } + + if audioParam.PayloadType != 110 { + t.Error("Audio PayloadType wrong:", audioParam.PayloadType) + } + + if audioParam.Ssrc != 162812879 { + t.Error("Audio Ssrc wrong", audioParam.Ssrc) + } + + if audioParam.Bitrate != 24 { + t.Error("Audio Bitrate wrong", audioParam.Bitrate) + } + + if audioParam.Interval != 5 { + t.Error("Audio Interval wrong", audioParam.Interval) + } + + if audioParam.MTU != 13 { + t.Error("Audio MTU wrong", audioParam.MTU) + } +} diff --git a/rtp/stream_controller_test.go b/rtp/stream_controller_test.go index 146af36..d4c2c6c 100644 --- a/rtp/stream_controller_test.go +++ b/rtp/stream_controller_test.go @@ -1,9 +1,10 @@ package rtp import ( + "testing" + "github.com/brutella/hap/characteristic" "github.com/brutella/hap/tlv8" - "testing" ) func TestStreamController(t *testing.T) { diff --git a/rtp/video_stream_configuration_test.go b/rtp/video_stream_configuration_test.go new file mode 100644 index 0000000..9e9035b --- /dev/null +++ b/rtp/video_stream_configuration_test.go @@ -0,0 +1,26 @@ +package rtp + +import ( + "reflect" + "testing" + + "github.com/brutella/hap/tlv8" +) + +func TestMarhsalUnmarshalDefaultVideoStreamConfiguration(t *testing.T) { + want := DefaultVideoStreamConfiguration() + buf, err := tlv8.Marshal(want) + if err != nil { + t.Fatal(err) + } + + var is VideoStreamConfiguration + err = tlv8.Unmarshal(buf, &is) + if err != nil { + t.Fatal(err) + } + + if reflect.DeepEqual(is, want) == false { + t.Fatalf("is=%+v want=%+v", is, want) + } +} diff --git a/server.go b/server.go index 89c01a1..bcca897 100644 --- a/server.go +++ b/server.go @@ -1,15 +1,17 @@ package hap import ( + "sync" + "time" + "github.com/brutella/dnssd" "github.com/brutella/hap/accessory" "github.com/brutella/hap/characteristic" "github.com/brutella/hap/log" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" - "golang.org/x/text/secure/precis" - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" + "github.com/xiam/to" + godiacritics "gopkg.in/Regis24GmbH/go-diacritics.v2" "bytes" "context" @@ -17,13 +19,11 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/xiam/to" "net" "net/http" "reflect" "strconv" "strings" - "unicode" ) // A server handles incoming HTTP request for an accessory. @@ -38,6 +38,10 @@ type Server struct { // If empty, a random port is used. Addr string + // Ifaces specifies at which interface the + // associated dnssd service is announced. + Ifaces []string + MfiCompliant bool // default false Protocol string // default "1.0" SetupId string @@ -57,6 +61,10 @@ type Server struct { // for dnssd stuff responder dnssd.Responder handle dnssd.ServiceHandle + + mux *sync.Mutex + sess map[string]interface{} + cons map[string]*conn } // A ServeMux lets you attach handlers to http url paths. @@ -65,6 +73,8 @@ type ServeMux interface { Handle(pattern string, handler http.Handler) // HandleFuncs registers the handler function for the given pattern. HandleFunc(pattern string, handler http.HandlerFunc) + // Mount attaches another http.Handler along ./pattern/* + Mount(pattern string, handler http.Handler) } // NewServer returns a new server given a store (to persist data) and accessories. @@ -79,13 +89,16 @@ func NewServer(store Store, a *accessory.A, as ...*accessory.A) (*Server, error) } s := &Server{ - ss: &http.Server{ - Handler: r, - ConnState: connStateEvent, - }, - st: st, - a: a, - as: as, + st: st, + a: a, + as: as, + mux: &sync.Mutex{}, + sess: make(map[string]interface{}), + cons: make(map[string]*conn), + } + s.ss = &http.Server{ + Handler: r, + ConnState: s.connStateEvent, } // Load the stored uuid or generate a new one. @@ -123,6 +136,7 @@ func NewServer(store Store, a *accessory.A, as ...*accessory.A) (*Server, error) r.Post("/pair-setup", s.pairSetup) r.Post("/pair-verify", s.pairVerify) r.Post("/identify", s.identify) + r.Post("/pairings", s.pairings) }) // The json encoded content is encrypted. The encryption keys @@ -132,7 +146,7 @@ func NewServer(store Store, a *accessory.A, as ...*accessory.A) (*Server, error) r.Get("/accessories", s.getAccessories) r.Get("/characteristics", s.getCharacteristics) r.Put("/characteristics", s.putCharacteristics) - r.Post("/pairings", s.pairings) + r.Put("/prepare", s.prepareCharacteristics) }) return s, nil @@ -146,10 +160,36 @@ func (s *Server) ServeMux() ServeMux { // IsAuthorized returns true if the provided // request is authorized to access accessory data. func (s *Server) IsAuthorized(request *http.Request) bool { - ss, _ := getSession(request.RemoteAddr) + ss, _ := s.getSession(request.RemoteAddr) return ss != nil } +func (s *Server) TimedWrite(request *http.Request) *TimedWrite { + if ss, _ := s.getSession(request.RemoteAddr); ss != nil { + return ss.twr + } + + return nil +} + +func (s *Server) SetTimedWrite(ttl, pid uint64, request *http.Request) { + if ss, _ := s.getSession(request.RemoteAddr); ss != nil { + t := time.Now().Add(time.Duration(ttl) * time.Millisecond) + ss.twr = &TimedWrite{t, pid} + } +} + +func (s *Server) DelTimedWrite(request *http.Request) { + if ss, _ := s.getSession(request.RemoteAddr); ss != nil { + ss.twr = nil + } +} + +// IsPaired returns true if the server is paired with a client (iOS). +func (s *Server) IsPaired() bool { + return len(s.st.Pairings()) > 0 +} + // ListenAndServe starts the server. func (s *Server) ListenAndServe(ctx context.Context) error { err := s.prepare() @@ -238,18 +278,34 @@ func (s *Server) add(as []*accessory.A) error { aid++ } + iids := map[uint64]interface{}{} var iid uint64 = 1 for _, s := range a.Ss { - s.Id = iid - iid++ + if s.Id == 0 { + s.Id = iid + iid++ + } + + if _, alreadyExists := iids[s.Id]; alreadyExists { + return fmt.Errorf("service id %d already exists (%s)", s.Id, a.Name()) + } + iids[s.Id] = struct{}{} for _, c := range s.Cs { // Create a local variable before // capturing them in a function. a := a - c.Id = iid - iid++ + if c.Id == 0 { + c.Id = iid + iid++ + } + + if _, alreadyExists := iids[c.Id]; alreadyExists { + return fmt.Errorf("characteristic id %d already exists (%s)", c.Id, a.Name()) + } + + iids[c.Id] = struct{}{} // If the value of a characteristic changes, we notify all connected clients. // The identify characteristic is a special case where we all accessory.IdentifyFunc. @@ -322,6 +378,75 @@ func (s *Server) prepare() error { return nil } +func (s *Server) connStateEvent(conn net.Conn, event http.ConnState) { + if event == http.StateClosed { + addr := conn.RemoteAddr().String() + s.mux.Lock() + delete(s.sess, addr) + delete(s.cons, addr) + s.mux.Unlock() + } +} + +func (s *Server) getSession(addr string) (*session, error) { + s.mux.Lock() + defer s.mux.Unlock() + + if v, ok := s.sess[addr]; ok { + if s, ok := v.(*session); ok { + return s, nil + } + return nil, fmt.Errorf("unexpected session %T", v) + } + + return nil, fmt.Errorf("no session for %s", addr) +} + +func (s *Server) getPairVerifySession(addr string) (*pairVerifySession, error) { + s.mux.Lock() + defer s.mux.Unlock() + + if v, ok := s.sess[addr]; ok { + if s, ok := v.(*pairVerifySession); ok { + return s, nil + } + return nil, fmt.Errorf("unexpected session %T", v) + } + + return nil, fmt.Errorf("no session for %s", addr) +} + +func (s *Server) getPairSetupSession(addr string) (*pairSetupSession, error) { + s.mux.Lock() + defer s.mux.Unlock() + + if v, ok := s.sess[addr]; ok { + if s, ok := v.(*pairSetupSession); ok { + return s, nil + } + return nil, fmt.Errorf("unexpected session %T", v) + } + + return nil, fmt.Errorf("no session for %s", addr) +} + +func (s *Server) setSession(addr string, v interface{}) { + s.mux.Lock() + s.sess[addr] = v + s.mux.Unlock() +} + +func (s *Server) sessions() map[string]interface{} { + copy := map[string]interface{}{} + s.mux.Lock() + for k, v := range s.sess { + copy[k] = v + } + s.mux.Unlock() + + return copy +} + func (s *Server) savePairing(p Pairing) error { err := s.st.SavePairing(p) if err != nil { @@ -342,8 +467,11 @@ func (s *Server) deletePairing(p Pairing) error { return nil } -func (s *Server) isPaired() bool { - return len(s.st.Pairings()) > 0 +func (s *Server) deleteAllPairings() { + for _, p := range s.st.Pairings() { + s.st.DeletePairing(p.Name) + } + s.updateTxtRecords() } func (s *Server) pairedWithAdmin() bool { @@ -362,7 +490,7 @@ func (s *Server) txtRecords() map[string]string { "id": s.uuid, "c#": fmt.Sprintf("%d", s.version), "s#": "1", - "sf": fmt.Sprintf("%d", to.Int64(!s.isPaired())), + "sf": fmt.Sprintf("%d", to.Int64(!s.IsPaired())), "ff": fmt.Sprintf("%d", to.Int64(s.MfiCompliant)), "md": s.a.Name(), "ci": fmt.Sprintf("%d", s.a.Type), @@ -392,14 +520,14 @@ func (s *Server) service() (dnssd.Service, error) { // // [Radar] http://openradar.appspot.com/radar?id=4931940373233664 stripped := strings.Replace(s.a.Info.Name.Value(), " ", "_", -1) - cfg := dnssd.Config{ - Name: removeAccentsFromString(stripped), + Name: normalize(stripped), Type: "_hap._tcp", Domain: "local", Host: strings.Replace(s.uuid, ":", "", -1), // use the id (without the colons) to get unique hostnames Text: s.txtRecords(), Port: s.port, + Ifaces: s.Ifaces, } return dnssd.NewService(cfg) @@ -428,19 +556,8 @@ func (s *Server) fmtPin() string { return first + "-" + second + "-" + third } -// RemoveAccentsFromString removes accent characters from string -// From https://stackoverflow.com/a/40405242/424814 -func removeAccentsFromString(v string) string { - var loosecompare = precis.NewIdentifier( - precis.AdditionalMapping(func() transform.Transformer { - return transform.Chain(norm.NFD, transform.RemoveFunc(func(r rune) bool { - return unicode.Is(unicode.Mn, r) - })) - }), - precis.Norm(norm.NFC), // This is the default; be explicit though. - ) - p, _ := loosecompare.String(v) - return p +func normalize(str string) string { + return godiacritics.Normalize(str) } func allZero(s []byte) bool { diff --git a/server_test.go b/server_test.go index 1878dd0..3caea74 100644 --- a/server_test.go +++ b/server_test.go @@ -2,6 +2,7 @@ package hap import ( "github.com/brutella/hap/accessory" + "github.com/brutella/hap/characteristic" "github.com/brutella/hap/service" "bytes" @@ -50,7 +51,7 @@ func TestIdentify(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/identify", nil) w := httptest.NewRecorder() - setSession(req.RemoteAddr, &session{}) + s.setSession(req.RemoteAddr, &session{}) var identified bool a.IdentifyFunc = func(r *http.Request) { @@ -84,16 +85,16 @@ func TestSetValueRequestSuccess(t *testing.T) { req := httptest.NewRequest(http.MethodPut, "/characteristics", bytes.NewBuffer([]byte(body))) w := httptest.NewRecorder() - setSession(req.RemoteAddr, &session{}) + s.setSession(req.RemoteAddr, &session{}) var setValueRequestFunc, onValueUpdateFunc bool - a.Outlet.On.SetValueRequestFunc = func(v interface{}, r *http.Request) int { + a.Outlet.On.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { if is, want := v.(bool), true; is != want { t.Fatalf("%v != %v", is, want) } setValueRequestFunc = true - return 0 + return v, 0 } a.Outlet.On.OnValueUpdate(func(new bool, old bool, r *http.Request) { @@ -128,6 +129,139 @@ func TestSetValueRequestSuccess(t *testing.T) { } } +func TestWriteResponseCharacteristic(t *testing.T) { + a := accessory.NewOutlet(accessory.Info{Name: "ABC"}) + c := characteristic.NewString("18") + c.Permissions = []string{characteristic.PermissionRead, characteristic.PermissionWrite, characteristic.PermissionWriteResponse} + a.Outlet.AddC(c.C) + + s, err := NewServer(NewMemStore(), a.A) + if err != nil { + t.Fatal(err) + } + + t.Run("put", func(t *testing.T) { + body := fmt.Sprintf("{\"characteristics\":[{\"aid\":%d,\"iid\":%d,\"value\":\"ABC\",\"r\":true}],\"pid\":0}", a.Id, c.Id) + req := httptest.NewRequest(http.MethodPut, "/characteristics", bytes.NewBuffer([]byte(body))) + w := httptest.NewRecorder() + + s.setSession(req.RemoteAddr, &session{}) + + setValueRequestFunc := false + c.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { + if is, want := v.(string), "ABC"; is != want { + t.Fatalf("%v != %v", is, want) + } + setValueRequestFunc = true + + return "DEF", 0 + } + + s.ss.Handler.ServeHTTP(w, req) + + r := w.Result() + if is, want := r.StatusCode, http.StatusMultiStatus; is != want { + t.Fatalf("%v != %v", is, want) + } + + if is, want := setValueRequestFunc, true; is != want { + t.Fatalf("%v != %v", is, want) + } + + // check reply body + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + body = fmt.Sprintf("{\"characteristics\":[{\"aid\":%d,\"iid\":%d,\"value\":\"DEF\",\"status\":0}]}", a.Id, c.Id) + if is, want := string(b), body; is != want { + t.Fatalf("%v != %v", is, want) + } + }) +} + +func TestPrepareValueRequest(t *testing.T) { + a := accessory.NewOutlet(accessory.Info{Name: "ABC"}) + a.Outlet.On.Permissions = append(a.Outlet.On.Permissions, characteristic.PermissionTimedWrite) + + s, err := NewServer(NewMemStore(), a.A) + if err != nil { + t.Fatal(err) + } + + t.Run("prepare", func(t *testing.T) { + body := fmt.Sprintf("{\"ttl\":500,\"pid\":123456789}") + req := httptest.NewRequest(http.MethodPut, "/prepare", bytes.NewBuffer([]byte(body))) + s.setSession(req.RemoteAddr, &session{}) + + w := httptest.NewRecorder() + + s.ss.Handler.ServeHTTP(w, req) + + r := w.Result() + if is, want := r.StatusCode, http.StatusOK; is != want { + t.Fatalf("%v != %v", is, want) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + if is, want := string(b), "{\"status\":0}"; is != want { + t.Fatalf("%v != %v", is, want) + } + }) + + t.Run("put", func(t *testing.T) { + body := fmt.Sprintf("{\"characteristics\":[{\"aid\":%d,\"iid\":%d,\"value\":true}],\"pid\":123456789}", a.Id, a.Outlet.On.Id) + req := httptest.NewRequest(http.MethodPut, "/characteristics", bytes.NewBuffer([]byte(body))) + w := httptest.NewRecorder() + + var setValueRequestFunc, onValueUpdateFunc bool + a.Outlet.On.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { + if is, want := v.(bool), true; is != want { + t.Fatalf("%v != %v", is, want) + } + setValueRequestFunc = true + + return v, 0 + } + + a.Outlet.On.OnValueUpdate(func(new bool, old bool, r *http.Request) { + if is, want := new, true; is != want { + t.Fatalf("%v != %v", is, want) + } + + if is, want := old, false; is != want { + t.Fatalf("%v != %v", is, want) + } + + onValueUpdateFunc = true + }) + + s.ss.Handler.ServeHTTP(w, req) + + r := w.Result() + if is, want := r.StatusCode, http.StatusNoContent; is != want { + t.Fatalf("%v != %v", is, want) + } + + if is, want := setValueRequestFunc, true; is != want { + t.Fatalf("%v != %v", is, want) + } + + if is, want := onValueUpdateFunc, true; is != want { + t.Fatalf("%v != %v", is, want) + } + + if is, want := a.Outlet.On.Value(), true; is != want { + t.Fatalf("%v != %v", is, want) + } + }) +} + func TestSetValueRequestFailure(t *testing.T) { a := accessory.NewOutlet(accessory.Info{Name: "ABC"}) @@ -140,10 +274,10 @@ func TestSetValueRequestFailure(t *testing.T) { req := httptest.NewRequest(http.MethodPut, "/characteristics", bytes.NewBuffer([]byte(body))) w := httptest.NewRecorder() - setSession(req.RemoteAddr, &session{}) + s.setSession(req.RemoteAddr, &session{}) - a.Outlet.On.SetValueRequestFunc = func(v interface{}, r *http.Request) int { - return JsonStatusResourceBusy + a.Outlet.On.SetValueRequestFunc = func(v interface{}, r *http.Request) (interface{}, int) { + return nil, JsonStatusResourceBusy } s.ss.Handler.ServeHTTP(w, req) @@ -167,3 +301,88 @@ func TestSetValueRequestFailure(t *testing.T) { t.Fatalf("%v != %v", is, want) } } + +func TestGetProgrammableSwitchEvent(t *testing.T) { + a := accessory.New(accessory.Info{Name: "ABC"}, accessory.TypeProgrammableSwitch) + s := service.NewStatelessProgrammableSwitch() + c := s.ProgrammableSwitchEvent + a.AddS(s.S) + srv, err := NewServer(NewMemStore(), a) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/characteristics?id=%d.%d", a.Id, c.Id), nil) + w := httptest.NewRecorder() + + srv.setSession(req.RemoteAddr, &session{}) + srv.ss.Handler.ServeHTTP(w, req) + + r := w.Result() + if is, want := r.StatusCode, http.StatusOK; is != want { + t.Fatalf("%v != %v", is, want) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + body := fmt.Sprintf("{\"characteristics\":[{\"aid\":%d,\"iid\":%d,\"value\":null}]}", a.Id, c.Id) + if is, want := string(b), body; is != want { + t.Fatalf("%v != %v", is, want) + } +} + +func TestGetValueRequestPartialFailure(t *testing.T) { + a := accessory.NewOutlet(accessory.Info{Name: "ABC"}) + sw1 := a.Outlet.On + sw2 := characteristic.NewOn() + a.Outlet.AddC(sw2.C) + + a.Outlet.On.ValueRequestFunc = func(r *http.Request) (interface{}, int) { + return nil, JsonStatusResourceBusy + } + + srv, err := NewServer(NewMemStore(), a.A) + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/characteristics?id=%d.%d,%[1]d.%[3]d", a.Id, sw1.Id, sw2.Id), nil) + w := httptest.NewRecorder() + + srv.setSession(req.RemoteAddr, &session{}) + srv.ss.Handler.ServeHTTP(w, req) + + r := w.Result() + if is, want := r.StatusCode, http.StatusMultiStatus; is != want { + t.Fatalf("%v != %v", is, want) + } + + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + + body := fmt.Sprintf("{\"characteristics\":[{\"aid\":%d,\"iid\":%d,\"status\":%d},{\"aid\":%[1]d,\"iid\":%[4]d,\"value\":false,\"status\":0}]}", a.Id, sw1.Id, JsonStatusResourceBusy, sw2.Id) + if is, want := string(b), body; is != want { + t.Fatalf("%v != %v", is, want) + } +} + +func TestStringNormalization(t *testing.T) { + tests := []struct { + is string + want string + }{ + {"daß", "dass"}, + {"Pâté", "Pate"}, + } + + for _, test := range tests { + if is, want := normalize(test.is), test.want; is != want { + t.Fatalf("%v != %v", is, want) + } + } +} diff --git a/service/accessory_runtime_information.go b/service/accessory_runtime_information.go new file mode 100644 index 0000000..f19f9da --- /dev/null +++ b/service/accessory_runtime_information.go @@ -0,0 +1,21 @@ +package service + +import "github.com/brutella/hap/characteristic" + +const TypeAccessoryRuntimeInformation = "239" + +type AccessoryRuntimeInformation struct { + *S + + Ping *characteristic.Ping +} + +func NewAccessoryRuntimeInformation() *AccessoryRuntimeInformation { + s := AccessoryRuntimeInformation{} + s.S = New(TypeAccessoryRuntimeInformation) + + s.Ping = characteristic.NewPing() + s.AddC(s.Ping.C) + + return &s +} diff --git a/service/protocol_information.go b/service/protocol_information.go new file mode 100644 index 0000000..5772e73 --- /dev/null +++ b/service/protocol_information.go @@ -0,0 +1,23 @@ +package service + +import ( + "github.com/brutella/hap/characteristic" +) + +const TypeProtocolInformation = "A2" + +type ProtocolInformation struct { + *S + + Version *characteristic.Version +} + +func NewProtocolInformation() *ProtocolInformation { + s := ProtocolInformation{} + s.S = New(TypeProtocolInformation) + + s.Version = characteristic.NewVersion() + s.AddC(s.Version.C) + + return &s +} diff --git a/session.go b/session.go index 90e4491..e0b1892 100644 --- a/session.go +++ b/session.go @@ -1,6 +1,8 @@ package hap import ( + "time" + "github.com/brutella/hap/chacha20poly1305" "github.com/brutella/hap/hkdf" @@ -8,84 +10,13 @@ import ( "encoding/binary" "fmt" "io" - "net" "net/http" "sync" ) var mux = &sync.Mutex{} -var sess = make(map[string]interface{}) var cons = make(map[string]*conn) -func connStateEvent(conn net.Conn, event http.ConnState) { - if event == http.StateClosed { - addr := conn.RemoteAddr().String() - mux.Lock() - delete(sess, addr) - delete(cons, addr) - mux.Unlock() - } -} - -func getSession(addr string) (*session, error) { - mux.Lock() - defer mux.Unlock() - - if v, ok := sess[addr]; ok { - if s, ok := v.(*session); ok { - return s, nil - } - return nil, fmt.Errorf("unexpected session %T", v) - } - - return nil, fmt.Errorf("no session for %s", addr) -} - -func getPairVerifySession(addr string) (*pairVerifySession, error) { - mux.Lock() - defer mux.Unlock() - - if v, ok := sess[addr]; ok { - if s, ok := v.(*pairVerifySession); ok { - return s, nil - } - return nil, fmt.Errorf("unexpected session %T", v) - } - - return nil, fmt.Errorf("no session for %s", addr) -} - -func getPairSetupSession(addr string) (*pairSetupSession, error) { - mux.Lock() - defer mux.Unlock() - - if v, ok := sess[addr]; ok { - if s, ok := v.(*pairSetupSession); ok { - return s, nil - } - return nil, fmt.Errorf("unexpected session %T", v) - } - - return nil, fmt.Errorf("no session for %s", addr) -} - -func setSession(addr string, v interface{}) { - mux.Lock() - sess[addr] = v - mux.Unlock() -} - -func sessions() map[string]interface{} { - copy := map[string]interface{}{} - mux.Lock() - for k, v := range sess { - copy[k] = v - } - mux.Unlock() - - return copy -} - func setConn(addr string, conn *conn) { mux.Lock() defer mux.Unlock() @@ -121,6 +52,14 @@ type session struct { decryptKey [32]byte encryptCount uint64 decryptCount uint64 + mu sync.Mutex + + twr *TimedWrite +} + +type TimedWrite struct { + deadline time.Time + pid uint64 } func newSession(shared [32]byte, p Pairing) (*session, error) { @@ -151,8 +90,10 @@ func (s *session) Encrypt(r io.Reader) (io.Reader, error) { var buf bytes.Buffer for _, p := range packets { var nonce [8]byte + s.mu.Lock() binary.LittleEndian.PutUint64(nonce[:], s.encryptCount) s.encryptCount++ + s.mu.Unlock() bLength := make([]byte, 2) binary.LittleEndian.PutUint16(bLength, uint16(p.length)) @@ -193,8 +134,10 @@ func (s *session) Decrypt(r io.Reader) (io.Reader, error) { } var nonce [8]byte + s.mu.Lock() binary.LittleEndian.PutUint64(nonce[:], s.decryptCount) s.decryptCount++ + s.mu.Unlock() lengthBytes := make([]byte, 2) binary.LittleEndian.PutUint16(lengthBytes, uint16(length)) diff --git a/tlv8/decoder.go b/tlv8/decoder.go index 42db823..eb718b5 100644 --- a/tlv8/decoder.go +++ b/tlv8/decoder.go @@ -57,16 +57,18 @@ func (d *decoder) decode(v interface{}) error { } for i := 0; i < eValue.NumField(); i++ { - if tlv8, ok := eType.Field(i).Tag.Lookup("tlv8"); ok { + typeField := eType.Field(i) + if tlv8, ok := typeField.Tag.Lookup("tlv8"); ok { values := strings.Split(tlv8, ",") tag := uint8(to.Uint64(values[0])) + optional := len(values) > 1 && values[1] == "optional" field := eValue.Field(i) switch value := field.Interface().(type) { case uint8: if v, err := d.r.readByte(tag); err == nil { field.SetUint(uint64(v)) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -74,7 +76,7 @@ func (d *decoder) decode(v interface{}) error { case uint16: if v, err := d.r.readUint16(tag); err == nil { field.SetUint(uint64(v)) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -83,7 +85,7 @@ func (d *decoder) decode(v interface{}) error { case int16: if v, err := d.r.readint16(tag); err == nil { field.SetInt(int64(v)) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -92,7 +94,7 @@ func (d *decoder) decode(v interface{}) error { case uint32: if v, err := d.r.readUint32(tag); err == nil { field.SetUint(uint64(v)) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -101,7 +103,7 @@ func (d *decoder) decode(v interface{}) error { case int32: if v, err := d.r.readint32(tag); err == nil { field.SetInt(int64(v)) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -110,7 +112,7 @@ func (d *decoder) decode(v interface{}) error { case int64: if v, err := d.r.readint64(tag); err == nil { field.SetInt(v) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -119,7 +121,7 @@ func (d *decoder) decode(v interface{}) error { case uint64: if v, err := d.r.readUint64(tag); err == nil { field.SetUint(v) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -128,7 +130,7 @@ func (d *decoder) decode(v interface{}) error { case float32: if v, err := d.r.readFloat32(tag); err == nil { field.SetFloat(float64(v)) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -137,7 +139,7 @@ func (d *decoder) decode(v interface{}) error { case []byte: if v, err := d.r.readBytes(tag); err == nil { field.SetBytes(v) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -146,7 +148,7 @@ func (d *decoder) decode(v interface{}) error { case string: if v, err := d.r.readString(tag); err == nil { field.SetString(v) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -154,7 +156,7 @@ func (d *decoder) decode(v interface{}) error { case bool: if v, err := d.r.readBool(tag); err == nil { field.SetBool(v) - } else if err == io.EOF { + } else if err == io.EOF && optional { continue } else { return err @@ -173,7 +175,7 @@ func (d *decoder) decode(v interface{}) error { if tlv8 == "-" { // unnamed slices are inline encoded err = d.decode(v) - if isEmptyStruct(v) { + if err == io.EOF { // step out of loop break } diff --git a/tlv8/encoder.go b/tlv8/encoder.go index 1e6c449..4201d28 100644 --- a/tlv8/encoder.go +++ b/tlv8/encoder.go @@ -2,8 +2,9 @@ package tlv8 import ( "bytes" - "github.com/xiam/to" "reflect" + + "github.com/xiam/to" ) type encoder struct {