Rewrite HomeKit client
This commit is contained in:
@@ -35,6 +35,14 @@ Requires ffmpeg built with `--enable-libfdk-aac`
|
||||
-acodec libfdk_aac -aprofile aac_eld
|
||||
```
|
||||
|
||||
| SampleRate | RTPTime | constantDuration | objectType |
|
||||
|------------|---------|--------------------|--------------|
|
||||
| 8000 | 60 | =8000/1000*60=480 | 39 (AAC ELD) |
|
||||
| 16000 | 30 | =16000/1000*30=480 | 39 (AAC ELD) |
|
||||
| 24000 | 20 | =24000/1000*20=480 | 39 (AAC ELD) |
|
||||
| 16000 | 60 | =16000/1000*60=960 | 23 (AAC LD) |
|
||||
| 24000 | 40 | =24000/1000*40=960 | 23 (AAC LD) |
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://github.com/apple/HomeKitADK/blob/master/Documentation/crypto.md
|
||||
|
||||
+124
-12
@@ -1,16 +1,64 @@
|
||||
package hap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
FormatString = "string"
|
||||
FormatBool = "bool"
|
||||
FormatFloat = "float"
|
||||
FormatUInt8 = "uint8"
|
||||
FormatUInt16 = "uint16"
|
||||
FormatUInt32 = "uint32"
|
||||
FormatInt32 = "int32"
|
||||
FormatUInt64 = "uint64"
|
||||
FormatData = "data"
|
||||
FormatTLV8 = "tlv8"
|
||||
|
||||
UnitPercentage = "percentage"
|
||||
)
|
||||
|
||||
var PR = []string{"pr"}
|
||||
var PW = []string{"pw"}
|
||||
var PRPW = []string{"pr", "pw"}
|
||||
var EVPRPW = []string{"ev", "pr", "pw"}
|
||||
var EVPR = []string{"ev", "pr"}
|
||||
|
||||
type Accessory struct {
|
||||
AID int `json:"aid"`
|
||||
AID uint8 `json:"aid"` // 150 unique accessories per bridge
|
||||
Services []*Service `json:"services"`
|
||||
}
|
||||
|
||||
type Accessories struct {
|
||||
Accessories []*Accessory `json:"accessories"`
|
||||
}
|
||||
func (a *Accessory) InitIID() {
|
||||
serviceN := map[string]byte{}
|
||||
for _, service := range a.Services {
|
||||
if len(service.Type) > 3 {
|
||||
panic(service.Type)
|
||||
}
|
||||
|
||||
type Characters struct {
|
||||
Characters []*Character `json:"characteristics"`
|
||||
n := serviceN[service.Type] + 1
|
||||
serviceN[service.Type] = n
|
||||
|
||||
if n > 15 {
|
||||
panic(n)
|
||||
}
|
||||
|
||||
// ServiceID = ANSSS000
|
||||
s := fmt.Sprintf("%x%x%03s000", a.AID, n, service.Type)
|
||||
service.IID, _ = strconv.ParseUint(s, 16, 64)
|
||||
|
||||
for _, character := range service.Characters {
|
||||
if len(character.Type) > 3 {
|
||||
panic(character.Type)
|
||||
}
|
||||
|
||||
// CharacterID = ANSSSCCC
|
||||
character.IID, _ = strconv.ParseUint(character.Type, 16, 64)
|
||||
character.IID += service.IID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Accessory) GetService(servType string) *Service {
|
||||
@@ -33,7 +81,7 @@ func (a *Accessory) GetCharacter(charType string) *Character {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Accessory) GetCharacterByID(iid int) *Character {
|
||||
func (a *Accessory) GetCharacterByID(iid uint64) *Character {
|
||||
for _, serv := range a.Services {
|
||||
for _, char := range serv.Characters {
|
||||
if char.IID == iid {
|
||||
@@ -45,11 +93,11 @@ func (a *Accessory) GetCharacterByID(iid int) *Character {
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
IID int `json:"iid"`
|
||||
Type string `json:"type"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
Hidden bool `json:"hidden,omitempty"`
|
||||
Characters []*Character `json:"characteristics"`
|
||||
Type string `json:"type"`
|
||||
IID uint64 `json:"iid"`
|
||||
Primary bool `json:"primary,omitempty"`
|
||||
Characters []*Character `json:"characteristics"`
|
||||
Linked []int `json:"linked,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Service) GetCharacter(charType string) *Character {
|
||||
@@ -60,3 +108,67 @@ func (s *Service) GetCharacter(charType string) *Character {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ServiceAccessoryInformation(manuf, model, name, serial, firmware string) *Service {
|
||||
return &Service{
|
||||
Type: "3E", // AccessoryInformation
|
||||
Characters: []*Character{
|
||||
{
|
||||
Type: "14",
|
||||
Format: FormatBool,
|
||||
Perms: PW,
|
||||
//Descr: "Identify",
|
||||
}, {
|
||||
Type: "20",
|
||||
Format: FormatString,
|
||||
Value: manuf,
|
||||
Perms: PR,
|
||||
//Descr: "Manufacturer",
|
||||
//MaxLen: 64,
|
||||
}, {
|
||||
Type: "21",
|
||||
Format: FormatString,
|
||||
Value: model,
|
||||
Perms: PR,
|
||||
//Descr: "Model",
|
||||
//MaxLen: 64,
|
||||
}, {
|
||||
Type: "23",
|
||||
Format: FormatString,
|
||||
Value: name,
|
||||
Perms: PR,
|
||||
//Descr: "Name",
|
||||
//MaxLen: 64,
|
||||
}, {
|
||||
Type: "30",
|
||||
Format: FormatString,
|
||||
Value: serial,
|
||||
Perms: PR,
|
||||
//Descr: "Serial Number",
|
||||
//MaxLen: 64,
|
||||
}, {
|
||||
Type: "52",
|
||||
Format: FormatString,
|
||||
Value: firmware,
|
||||
Perms: PR,
|
||||
//Descr: "Firmware Revision",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ServiceHAPProtocolInformation() *Service {
|
||||
return &Service{
|
||||
Type: "A2", // 'HAPProtocolInformation'
|
||||
Characters: []*Character{
|
||||
{
|
||||
Type: "37",
|
||||
Format: FormatString,
|
||||
Value: "1.1.0",
|
||||
Perms: PR,
|
||||
//Descr: "Version",
|
||||
//MaxLen: 64,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ package camera
|
||||
const TypeSupportedVideoStreamConfiguration = "114"
|
||||
|
||||
type SupportedVideoStreamConfig struct {
|
||||
Codecs []VideoCodecConfig `tlv8:"1"`
|
||||
Codecs []VideoCodec `tlv8:"1"`
|
||||
}
|
||||
|
||||
type VideoCodecConfig struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []VideoCodecParams `tlv8:"2"`
|
||||
VideoAttrs []VideoAttrs `tlv8:"3"`
|
||||
type VideoCodec struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []VideoParams `tlv8:"2"`
|
||||
VideoAttrs []VideoAttrs `tlv8:"3"`
|
||||
RTPParams []RTPParams `tlv8:"4"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
VideoCodecTypeH264 = 0
|
||||
|
||||
@@ -29,12 +31,12 @@ const (
|
||||
VideoCodecCvoSuppported = 1
|
||||
)
|
||||
|
||||
type VideoCodecParams struct {
|
||||
ProfileID byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high
|
||||
Level byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0
|
||||
PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved
|
||||
CVOEnabled byte `tlv8:"4"` // 0 - not supported, 1 - supported
|
||||
CVOID byte `tlv8:"5"` // ???
|
||||
type VideoParams struct {
|
||||
ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high
|
||||
Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0
|
||||
PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved
|
||||
CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported
|
||||
CVOID []byte `tlv8:"5"` // ???
|
||||
}
|
||||
|
||||
type VideoAttrs struct {
|
||||
|
||||
@@ -3,10 +3,11 @@ package camera
|
||||
const TypeSupportedAudioStreamConfiguration = "115"
|
||||
|
||||
type SupportedAudioStreamConfig struct {
|
||||
Codecs []AudioCodecConfig `tlv8:"1"`
|
||||
ComfortNoise byte `tlv8:"2"`
|
||||
Codecs []AudioCodec `tlv8:"1"`
|
||||
ComfortNoise byte `tlv8:"2"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
AudioCodecTypePCMU = 0
|
||||
AudioCodecTypePCMA = 1
|
||||
@@ -22,16 +23,24 @@ const (
|
||||
AudioCodecSampleRate8Khz = 0
|
||||
AudioCodecSampleRate16Khz = 1
|
||||
AudioCodecSampleRate24Khz = 2
|
||||
|
||||
RTPTimeAACELD8 = 60 // 8000/1000*60=480
|
||||
RTPTimeAACELD16 = 30 // 16000/1000*30=480
|
||||
RTPTimeAACELD24 = 20 // 24000/1000*20=480
|
||||
RTPTimeAACLD16 = 60 // 16000/1000*60=960
|
||||
RTPTimeAACLD24 = 40 // 24000/1000*40=960
|
||||
)
|
||||
|
||||
type AudioCodecConfig struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []AudioCodecParams `tlv8:"2"`
|
||||
type AudioCodec struct {
|
||||
CodecType byte `tlv8:"1"`
|
||||
CodecParams []AudioParams `tlv8:"2"`
|
||||
RTPParams []RTPParams `tlv8:"3"`
|
||||
ComfortNoise []byte `tlv8:"4"`
|
||||
}
|
||||
|
||||
type AudioCodecParams struct {
|
||||
Channels byte `tlv8:"1"`
|
||||
Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant
|
||||
SampleRate byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
|
||||
RTPTime byte `tlv8:"4"`
|
||||
type AudioParams struct {
|
||||
Channels uint8 `tlv8:"1"`
|
||||
Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant
|
||||
SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000
|
||||
RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package camera
|
||||
|
||||
const TypeSupportedRTPConfiguration = "116"
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
CryptoAES_CM_128_HMAC_SHA1_80 = 0
|
||||
CryptoAES_CM_256_HMAC_SHA1_80 = 1
|
||||
CryptoNone = 2
|
||||
)
|
||||
|
||||
type SupportedRTPConfig struct {
|
||||
CryptoType []byte `tlv8:"2"`
|
||||
}
|
||||
@@ -3,11 +3,12 @@ package camera
|
||||
const TypeSelectedStreamConfiguration = "117"
|
||||
|
||||
type SelectedStreamConfig struct {
|
||||
Control SessionControl `tlv8:"1"`
|
||||
VideoParams SelectedVideoParams `tlv8:"2"`
|
||||
AudioParams SelectedAudioParams `tlv8:"3"`
|
||||
Control SessionControl `tlv8:"1"`
|
||||
VideoCodec VideoCodec `tlv8:"2"`
|
||||
AudioCodec AudioCodec `tlv8:"3"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
SessionCommandEnd = 0
|
||||
SessionCommandStart = 1
|
||||
@@ -17,36 +18,15 @@ const (
|
||||
)
|
||||
|
||||
type SessionControl struct {
|
||||
Session string `tlv8:"1"`
|
||||
Command byte `tlv8:"2"`
|
||||
SessionID string `tlv8:"1"`
|
||||
Command byte `tlv8:"2"`
|
||||
}
|
||||
|
||||
type SelectedVideoParams struct {
|
||||
CodecType byte `tlv8:"1"` // only 0 - H264
|
||||
CodecParams VideoCodecParams `tlv8:"2"`
|
||||
VideoAttrs VideoAttrs `tlv8:"3"`
|
||||
RTPParams VideoRTPParams `tlv8:"4"`
|
||||
}
|
||||
|
||||
type VideoRTPParams struct {
|
||||
PayloadType uint8 `tlv8:"1"`
|
||||
SSRC uint32 `tlv8:"2"`
|
||||
MaxBitrate uint16 `tlv8:"3"`
|
||||
MinRTCPInterval float32 `tlv8:"4"`
|
||||
MaxMTU uint16 `tlv8:"5"`
|
||||
}
|
||||
|
||||
type SelectedAudioParams struct {
|
||||
CodecType byte `tlv8:"1"` // 2 - AAC_ELD, 3 - OPUS, 5 - AMR, 6 - AMR_WB
|
||||
CodecParams AudioCodecParams `tlv8:"2"`
|
||||
RTPParams AudioRTPParams `tlv8:"3"`
|
||||
ComfortNoise uint8 `tlv8:"4"`
|
||||
}
|
||||
|
||||
type AudioRTPParams struct {
|
||||
PayloadType uint8 `tlv8:"1"`
|
||||
SSRC uint32 `tlv8:"2"`
|
||||
MaxBitrate uint16 `tlv8:"3"`
|
||||
MinRTCPInterval float32 `tlv8:"4"`
|
||||
ComfortNoisePayloadType uint8 `tlv8:"6"`
|
||||
type RTPParams struct {
|
||||
PayloadType uint8 `tlv8:"1"`
|
||||
SSRC uint32 `tlv8:"2"`
|
||||
MaxBitrate uint16 `tlv8:"3"`
|
||||
MinRTCPInterval float32 `tlv8:"4"`
|
||||
MaxMTU []uint16 `tlv8:"5"`
|
||||
ComfortNoisePayloadType []uint8 `tlv8:"6"`
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ package camera
|
||||
const TypeSetupEndpoints = "118"
|
||||
|
||||
type SetupEndpoints struct {
|
||||
SessionID []byte `tlv8:"1"`
|
||||
ControllerAddr Addr `tlv8:"3"`
|
||||
VideoCrypto CryptoSuite `tlv8:"4"`
|
||||
AudioCrypto CryptoSuite `tlv8:"5"`
|
||||
SessionID string `tlv8:"1"`
|
||||
Status []byte `tlv8:"2"`
|
||||
Address Addr `tlv8:"3"`
|
||||
VideoCrypto CryptoSuite `tlv8:"4"`
|
||||
AudioCrypto CryptoSuite `tlv8:"5"`
|
||||
VideoSSRC []uint32 `tlv8:"6"`
|
||||
AudioSSRC []uint32 `tlv8:"7"`
|
||||
}
|
||||
|
||||
type Addr struct {
|
||||
@@ -18,16 +21,6 @@ type Addr struct {
|
||||
|
||||
type CryptoSuite struct {
|
||||
CryptoType byte `tlv8:"1"`
|
||||
MasterKey []byte `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
|
||||
MasterSalt []byte `tlv8:"3"` // 14 byte
|
||||
}
|
||||
|
||||
type SetupEndpointsResponse struct {
|
||||
SessionID []byte `tlv8:"1"`
|
||||
Status byte `tlv8:"2"`
|
||||
AccessoryAddr Addr `tlv8:"3"`
|
||||
VideoCrypto CryptoSuite `tlv8:"4"`
|
||||
AudioCrypto CryptoSuite `tlv8:"5"`
|
||||
VideoSSRC uint32 `tlv8:"6"`
|
||||
AudioSSRC uint32 `tlv8:"7"`
|
||||
MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM)
|
||||
MasterSalt string `tlv8:"3"` // 14 byte
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ type StreamingStatus struct {
|
||||
Status byte `tlv8:"1"`
|
||||
}
|
||||
|
||||
//goland:noinspection ALL
|
||||
const (
|
||||
StreamingStatusAvailable = 0
|
||||
StreamingStatusBusy = 1
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
client *hap.Client
|
||||
}
|
||||
|
||||
func NewClient(client *hap.Client) *Client {
|
||||
return &Client{client: client}
|
||||
}
|
||||
|
||||
func (c *Client) StartStream(ses *Session) error {
|
||||
// Step 1. Check if camera ready (free) to stream
|
||||
srv, err := c.GetFreeStream()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if srv == nil {
|
||||
return errors.New("no free streams")
|
||||
}
|
||||
|
||||
if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.SetConfig(srv, ses.Config)
|
||||
}
|
||||
|
||||
// GetFreeStream search free streaming service.
|
||||
// Usual every HomeKit camera can stream only to two clients simultaniosly.
|
||||
// So it has two similar services for streaming.
|
||||
func (c *Client) GetFreeStream() (srv *hap.Service, err error) {
|
||||
accs, err := c.client.GetAccessories()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, srv = range accs[0].Services {
|
||||
for _, char := range srv.Characters {
|
||||
if char.Type == TypeStreamingStatus {
|
||||
var status StreamingStatus
|
||||
if err = char.ReadTLV8(&status); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if status.Status == StreamingStatusAvailable {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetupEndpoins(srv *hap.Service, req *SetupEndpoints) (res *SetupEndpointsResponse, err error) {
|
||||
// get setup endpoint character ID
|
||||
char := srv.GetCharacter(TypeSetupEndpoints)
|
||||
char.Event = nil
|
||||
// encode new character value
|
||||
if err = char.Write(req); err != nil {
|
||||
return
|
||||
}
|
||||
// write (put) new endpoint value to device
|
||||
if err = c.client.PutCharacters(char); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// get new endpoint value from device (response)
|
||||
if err = c.client.GetCharacter(char); err != nil {
|
||||
return
|
||||
}
|
||||
// decode new endpoint value
|
||||
res = &SetupEndpointsResponse{}
|
||||
if err = char.ReadTLV8(res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) SetConfig(srv *hap.Service, config *SelectedStreamConfig) error {
|
||||
// get setup endpoint character ID
|
||||
char := srv.GetCharacter(TypeSelectedStreamConfiguration)
|
||||
char.Event = nil
|
||||
// encode new character value
|
||||
if err := char.Write(config); err != nil {
|
||||
return err
|
||||
}
|
||||
// write (put) new character value to device
|
||||
return c.client.PutCharacters(char)
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Offer *SetupEndpoints
|
||||
Answer *SetupEndpointsResponse
|
||||
Config *SelectedStreamConfig
|
||||
}
|
||||
|
||||
func NewSession(vp *SelectedVideoParams, ap *SelectedAudioParams) *Session {
|
||||
vp.RTPParams = VideoRTPParams{
|
||||
PayloadType: 99,
|
||||
SSRC: RandomUint32(),
|
||||
MaxBitrate: 2048,
|
||||
MinRTCPInterval: 10,
|
||||
MaxMTU: 1200, // like WebRTC
|
||||
}
|
||||
ap.RTPParams = AudioRTPParams{
|
||||
PayloadType: 110,
|
||||
SSRC: RandomUint32(),
|
||||
MaxBitrate: 32,
|
||||
MinRTCPInterval: 10,
|
||||
ComfortNoisePayloadType: 98,
|
||||
}
|
||||
|
||||
sessionID := RandomBytes(16)
|
||||
s := &Session{
|
||||
Offer: &SetupEndpoints{
|
||||
SessionID: sessionID,
|
||||
VideoCrypto: CryptoSuite{
|
||||
MasterKey: RandomBytes(16),
|
||||
MasterSalt: RandomBytes(14),
|
||||
},
|
||||
AudioCrypto: CryptoSuite{
|
||||
MasterKey: RandomBytes(16),
|
||||
MasterSalt: RandomBytes(14),
|
||||
},
|
||||
},
|
||||
Config: &SelectedStreamConfig{
|
||||
Control: SessionControl{
|
||||
Session: string(sessionID),
|
||||
Command: SessionCommandStart,
|
||||
},
|
||||
VideoParams: *vp,
|
||||
AudioParams: *ap,
|
||||
},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Session) SetLocalEndpoint(host string, port uint16) {
|
||||
s.Offer.ControllerAddr = Addr{
|
||||
IPAddr: host,
|
||||
VideoRTPPort: port,
|
||||
AudioRTPPort: port,
|
||||
}
|
||||
}
|
||||
|
||||
func RandomBytes(size int) []byte {
|
||||
data := make([]byte, size)
|
||||
_, _ = rand.Read(data)
|
||||
return data
|
||||
}
|
||||
|
||||
func RandomUint32() uint32 {
|
||||
data := make([]byte, 4)
|
||||
_, _ = rand.Read(data)
|
||||
return binary.BigEndian.Uint32(data)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package camera
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
)
|
||||
|
||||
type Stream struct {
|
||||
id string
|
||||
client *hap.Client
|
||||
service *hap.Service
|
||||
}
|
||||
|
||||
func NewStream(
|
||||
client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, videoSession, audioSession *srtp.Session,
|
||||
) (*Stream, error) {
|
||||
stream := &Stream{
|
||||
id: core.RandString(16, 0),
|
||||
client: client,
|
||||
}
|
||||
|
||||
if err := stream.GetFreeStream(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := stream.ExchangeEndpoints(videoSession, audioSession); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
videoCodec.RTPParams = []RTPParams{
|
||||
{
|
||||
PayloadType: 99,
|
||||
SSRC: videoSession.Local.SSRC,
|
||||
MaxBitrate: 299,
|
||||
MinRTCPInterval: 0.5,
|
||||
MaxMTU: []uint16{1378},
|
||||
},
|
||||
}
|
||||
audioCodec.RTPParams = []RTPParams{
|
||||
{
|
||||
PayloadType: 110,
|
||||
SSRC: audioSession.Local.SSRC,
|
||||
MaxBitrate: 24,
|
||||
MinRTCPInterval: 5,
|
||||
|
||||
ComfortNoisePayloadType: []uint8{13},
|
||||
},
|
||||
}
|
||||
audioCodec.ComfortNoise = []byte{0}
|
||||
|
||||
config := &SelectedStreamConfig{
|
||||
Control: SessionControl{
|
||||
SessionID: stream.id,
|
||||
Command: SessionCommandStart,
|
||||
},
|
||||
VideoCodec: *videoCodec,
|
||||
AudioCodec: *audioCodec,
|
||||
}
|
||||
|
||||
if err := stream.SetStreamConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
// GetFreeStream search free streaming service.
|
||||
// Usual every HomeKit camera can stream only to two clients simultaniosly.
|
||||
// So it has two similar services for streaming.
|
||||
func (s *Stream) GetFreeStream() error {
|
||||
acc, err := s.client.GetFirstAccessory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, srv := range acc.Services {
|
||||
for _, char := range srv.Characters {
|
||||
if char.Type == TypeStreamingStatus {
|
||||
var status StreamingStatus
|
||||
if err = char.ReadTLV8(&status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if status.Status == StreamingStatusAvailable {
|
||||
s.service = srv
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("hap: no free streams")
|
||||
}
|
||||
|
||||
func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error {
|
||||
req := SetupEndpoints{
|
||||
SessionID: s.id,
|
||||
Address: Addr{
|
||||
IPVersion: 0,
|
||||
IPAddr: videoSession.Local.Addr,
|
||||
VideoRTPPort: videoSession.Local.Port,
|
||||
AudioRTPPort: audioSession.Local.Port,
|
||||
},
|
||||
VideoCrypto: CryptoSuite{
|
||||
MasterKey: string(videoSession.Local.MasterKey),
|
||||
MasterSalt: string(videoSession.Local.MasterSalt),
|
||||
},
|
||||
AudioCrypto: CryptoSuite{
|
||||
MasterKey: string(audioSession.Local.MasterKey),
|
||||
MasterSalt: string(audioSession.Local.MasterSalt),
|
||||
},
|
||||
}
|
||||
|
||||
char := s.service.GetCharacter(TypeSetupEndpoints)
|
||||
if err := char.Write(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.client.PutCharacters(char); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var res SetupEndpoints
|
||||
if err := s.client.GetCharacter(char); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := char.ReadTLV8(&res); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
videoSession.Remote = &srtp.Endpoint{
|
||||
Addr: res.Address.IPAddr,
|
||||
Port: res.Address.VideoRTPPort,
|
||||
MasterKey: []byte(res.VideoCrypto.MasterKey),
|
||||
MasterSalt: []byte(res.VideoCrypto.MasterSalt),
|
||||
SSRC: res.VideoSSRC[0],
|
||||
}
|
||||
|
||||
audioSession.Remote = &srtp.Endpoint{
|
||||
Addr: res.Address.IPAddr,
|
||||
Port: res.Address.AudioRTPPort,
|
||||
MasterKey: []byte(res.AudioCrypto.MasterKey),
|
||||
MasterSalt: []byte(res.AudioCrypto.MasterSalt),
|
||||
SSRC: res.AudioSSRC[0],
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error {
|
||||
char := s.service.GetCharacter(TypeSelectedStreamConfiguration)
|
||||
if err := char.Write(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.client.PutCharacters(char); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.client.GetCharacter(char)
|
||||
}
|
||||
|
||||
func (s *Stream) Close() error {
|
||||
config := &SelectedStreamConfig{
|
||||
Control: SessionControl{
|
||||
SessionID: s.id,
|
||||
Command: SessionCommandEnd,
|
||||
},
|
||||
}
|
||||
|
||||
char := s.service.GetCharacter(TypeSelectedStreamConfiguration)
|
||||
if err := char.Write(config); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.client.PutCharacters(char)
|
||||
}
|
||||
+21
-12
@@ -9,16 +9,23 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
)
|
||||
|
||||
// Character - Aqara props order
|
||||
// Value should be omit for PW
|
||||
// Value may be empty for PR
|
||||
type Character struct {
|
||||
AID int `json:"aid,omitempty"`
|
||||
IID int `json:"iid"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Event any `json:"ev,omitempty"`
|
||||
Perms []string `json:"perms,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
//MaxDataLen int `json:"maxDataLen"`
|
||||
IID uint64 `json:"iid"`
|
||||
Type string `json:"type"`
|
||||
Format string `json:"format"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Perms []string `json:"perms"`
|
||||
|
||||
//Descr string `json:"description,omitempty"`
|
||||
//MaxLen int `json:"maxLen,omitempty"`
|
||||
//Unit string `json:"unit,omitempty"`
|
||||
//MinValue any `json:"minValue,omitempty"`
|
||||
//MaxValue any `json:"maxValue,omitempty"`
|
||||
//MinStep any `json:"minStep,omitempty"`
|
||||
//ValidVal []any `json:"valid-values,omitempty"`
|
||||
|
||||
listeners map[io.Writer]bool
|
||||
}
|
||||
@@ -64,10 +71,12 @@ func (c *Character) NotifyListeners(ignore io.Writer) error {
|
||||
|
||||
// GenerateEvent with raw HTTP headers
|
||||
func (c *Character) GenerateEvent() (data []byte, err error) {
|
||||
chars := Characters{
|
||||
Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}},
|
||||
v := JSONCharacters{
|
||||
Value: []JSONCharacter{
|
||||
{AID: DeviceAID, IID: c.IID, Value: c.Value},
|
||||
},
|
||||
}
|
||||
if data, err = json.Marshal(chars); err != nil {
|
||||
if data, err = json.Marshal(v); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+53
-48
@@ -37,9 +37,9 @@ type Client struct {
|
||||
ClientPrivate []byte
|
||||
|
||||
OnEvent func(res *http.Response)
|
||||
Output func(msg any)
|
||||
//Output func(msg any)
|
||||
|
||||
conn net.Conn
|
||||
Conn net.Conn
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
@@ -89,21 +89,21 @@ func (c *Client) Dial() (err error) {
|
||||
return false
|
||||
})
|
||||
|
||||
if c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
|
||||
if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
// STEP M1: send our session public to device
|
||||
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
||||
|
||||
// 1. Send sessionPublic
|
||||
plainM1 := struct {
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
PublicKey: sessionPublic,
|
||||
PublicKey: string(sessionPublic),
|
||||
State: StateM1,
|
||||
}
|
||||
res, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1))
|
||||
@@ -113,19 +113,19 @@ func (c *Client) Dial() (err error) {
|
||||
|
||||
// STEP M2: unpack deviceID from response
|
||||
var cipherM2 struct {
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}
|
||||
if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil {
|
||||
return err
|
||||
}
|
||||
if cipherM2.State != StateM2 {
|
||||
return NewResponseError(plainM1, cipherM2)
|
||||
return newResponseError(plainM1, cipherM2)
|
||||
}
|
||||
|
||||
// 1. generate session shared key
|
||||
sessionShared, err := curve25519.SharedSecret(sessionPrivate, cipherM2.PublicKey)
|
||||
sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(cipherM2.PublicKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (c *Client) Dial() (err error) {
|
||||
}
|
||||
|
||||
// 2. decrypt M2 response with session key
|
||||
b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", cipherM2.EncryptedData)
|
||||
b, err := chacha20poly1305.Decrypt(sessionKey, "PV-Msg02", []byte(cipherM2.EncryptedData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (c *Client) Dial() (err error) {
|
||||
// 3. unpack payload from TLV8
|
||||
var plainM2 struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
Signature string `tlv8:"10"`
|
||||
}
|
||||
if err = tlv8.Unmarshal(b, &plainM2); err != nil {
|
||||
return
|
||||
@@ -156,7 +156,7 @@ func (c *Client) Dial() (err error) {
|
||||
// device session + device id + our session
|
||||
if c.DevicePublic != nil {
|
||||
b = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic)
|
||||
if !ed25519.ValidateSignature(c.DevicePublic, b, plainM2.Signature) {
|
||||
if !ed25519.ValidateSignature(c.DevicePublic, b, []byte(plainM2.Signature)) {
|
||||
return errors.New("hap: ValidateSignature")
|
||||
}
|
||||
}
|
||||
@@ -172,10 +172,10 @@ func (c *Client) Dial() (err error) {
|
||||
// 2. generate payload
|
||||
plainM3 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
Signature: b,
|
||||
Signature: string(b),
|
||||
}
|
||||
if b, err = tlv8.Marshal(plainM3); err != nil {
|
||||
return
|
||||
@@ -188,11 +188,11 @@ func (c *Client) Dial() (err error) {
|
||||
|
||||
// 4. generate request
|
||||
cipherM3 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
State: StateM3,
|
||||
EncryptedData: b,
|
||||
EncryptedData: string(b),
|
||||
}
|
||||
if res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil {
|
||||
return
|
||||
@@ -206,25 +206,25 @@ func (c *Client) Dial() (err error) {
|
||||
return
|
||||
}
|
||||
if plainM4.State != StateM4 {
|
||||
return NewResponseError(cipherM3, plainM4)
|
||||
return newResponseError(cipherM3, plainM4)
|
||||
}
|
||||
|
||||
// like tls.Client wrapper over net.Conn
|
||||
if c.conn, err = secure.Client(c.conn, sessionShared, true); err != nil {
|
||||
if c.Conn, err = secure.Client(c.Conn, sessionShared, true); err != nil {
|
||||
return
|
||||
}
|
||||
// new reader for new conn
|
||||
c.reader = bufio.NewReaderSize(c.conn, 32*1024) // 32K like default request body
|
||||
c.reader = bufio.NewReaderSize(c.Conn, 32*1024) // 32K like default request body
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.conn == nil {
|
||||
if c.Conn == nil {
|
||||
return nil
|
||||
}
|
||||
conn := c.conn
|
||||
c.conn = nil
|
||||
conn := c.Conn
|
||||
c.Conn = nil
|
||||
return conn.Close()
|
||||
}
|
||||
|
||||
@@ -234,23 +234,26 @@ func (c *Client) GetAccessories() ([]*Accessory, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ac Accessories
|
||||
if err = json.NewDecoder(res.Body).Decode(&ac); err != nil {
|
||||
var v JSONAccessories
|
||||
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, accs := range ac.Accessories {
|
||||
for _, serv := range accs.Services {
|
||||
for _, char := range serv.Characters {
|
||||
char.AID = accs.AID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ac.Accessories, nil
|
||||
return v.Value, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCharacters(query string) ([]*Character, error) {
|
||||
func (c *Client) GetFirstAccessory() (*Accessory, error) {
|
||||
accs, err := c.GetAccessories()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(accs) == 0 {
|
||||
return nil, errors.New("hap: GetAccessories zero answer")
|
||||
}
|
||||
return accs[0], nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCharacters(query string) ([]JSONCharacter, error) {
|
||||
res, err := c.Get(PathCharacteristics + "?id=" + query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -261,15 +264,15 @@ func (c *Client) GetCharacters(query string) ([]*Character, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ch Characters
|
||||
if err = json.Unmarshal(data, &ch); err != nil {
|
||||
var v JSONCharacters
|
||||
if err = json.Unmarshal(data, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ch.Characters, nil
|
||||
return v.Value, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetCharacter(char *Character) error {
|
||||
query := fmt.Sprintf("%d.%d", char.AID, char.IID)
|
||||
query := fmt.Sprintf("%d.%d", DeviceAID, char.IID)
|
||||
chars, err := c.GetCharacters(query)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -279,20 +282,21 @@ func (c *Client) GetCharacter(char *Character) error {
|
||||
}
|
||||
|
||||
func (c *Client) PutCharacters(characters ...*Character) error {
|
||||
var v JSONCharacters
|
||||
for i, char := range characters {
|
||||
if char.Event != nil {
|
||||
char = &Character{AID: char.AID, IID: char.IID, Event: char.Event}
|
||||
} else {
|
||||
char = &Character{AID: char.AID, IID: char.IID, Value: char.Value}
|
||||
}
|
||||
v.Value = append(v.Value, JSONCharacter{
|
||||
AID: 1,
|
||||
IID: char.IID,
|
||||
Value: char.Value,
|
||||
})
|
||||
characters[i] = char
|
||||
}
|
||||
data, err := json.Marshal(Characters{characters})
|
||||
body, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(data))
|
||||
_, err = c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -312,8 +316,9 @@ func (c *Client) GetImage(width, height int) ([]byte, error) {
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
func (c *Client) LocalAddr() string {
|
||||
return c.conn.LocalAddr().String()
|
||||
func (c *Client) LocalIP() string {
|
||||
addr := c.Conn.LocalAddr().(*net.TCPAddr)
|
||||
return addr.IP.To4().String()
|
||||
}
|
||||
|
||||
func DecodeKey(s string) []byte {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -20,10 +19,7 @@ const (
|
||||
)
|
||||
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
if err := c.conn.SetWriteDeadline(time.Now().Add(ConnDeadline)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := req.Write(c.conn); err != nil {
|
||||
if err := req.Write(c.Conn); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return http.ReadResponse(c.reader, req)
|
||||
|
||||
+36
-41
@@ -4,9 +4,7 @@ import (
|
||||
"bufio"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
|
||||
@@ -21,9 +19,9 @@ func Pair(deviceID, pin string) (*Client, error) {
|
||||
var mfi bool
|
||||
|
||||
_ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||
if entry.Complete() && entry.Info["id"] == deviceID {
|
||||
if entry.Complete() && entry.Info[TXTDeviceID] == deviceID {
|
||||
addr = entry.Addr()
|
||||
mfi = entry.Info["ff"] == "1"
|
||||
mfi = entry.Info[TXTFeatureFlags] == "1"
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -44,19 +42,16 @@ func Pair(deviceID, pin string) (*Client, error) {
|
||||
}
|
||||
|
||||
func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
pin = strings.ReplaceAll(pin, "-", "")
|
||||
if len(pin) != 8 {
|
||||
return fmt.Errorf("wrong PIN format: %s", pin)
|
||||
if pin, err = SanitizePin(pin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] // 123-45-678
|
||||
|
||||
c.conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout)
|
||||
c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.reader = bufio.NewReader(c.conn)
|
||||
c.reader = bufio.NewReader(c.Conn)
|
||||
|
||||
// STEP M1. Send HELLO
|
||||
plainM1 := struct {
|
||||
@@ -76,8 +71,8 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
|
||||
// STEP M2. Read Device Salt and session PublicKey
|
||||
var plainM2 struct {
|
||||
Salt []byte `tlv8:"2"`
|
||||
SessionKey []byte `tlv8:"3"` // server public key, aka session.B
|
||||
Salt string `tlv8:"2"`
|
||||
SessionKey string `tlv8:"3"` // server public key, aka session.B
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}
|
||||
@@ -85,7 +80,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
return
|
||||
}
|
||||
if plainM2.State != StateM2 {
|
||||
return NewResponseError(plainM1, plainM2)
|
||||
return newResponseError(plainM1, plainM2)
|
||||
}
|
||||
if plainM2.Error != 0 {
|
||||
return newPairingError(plainM2.Error)
|
||||
@@ -106,19 +101,19 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
|
||||
// username: "Pair-Setup", password: PIN (with dashes)
|
||||
session := pake.NewClientSession(username, []byte(pin))
|
||||
sessionShared, err := session.ComputeKey(plainM2.Salt, plainM2.SessionKey)
|
||||
sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// STEP M3. Send request
|
||||
plainM3 := struct {
|
||||
SessionKey []byte `tlv8:"3"`
|
||||
Proof []byte `tlv8:"4"`
|
||||
SessionKey string `tlv8:"3"`
|
||||
Proof string `tlv8:"4"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
SessionKey: session.GetA(), // client public key, aka session.A
|
||||
Proof: session.ComputeAuthenticator(),
|
||||
SessionKey: string(session.GetA()), // client public key, aka session.A
|
||||
Proof: string(session.ComputeAuthenticator()),
|
||||
State: StateM3,
|
||||
}
|
||||
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil {
|
||||
@@ -127,7 +122,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
|
||||
// STEP M4. Read response
|
||||
var plainM4 struct {
|
||||
Proof []byte `tlv8:"4"` // server proof
|
||||
Proof string `tlv8:"4"` // server proof
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}
|
||||
@@ -135,15 +130,15 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
return
|
||||
}
|
||||
if plainM4.State != StateM4 {
|
||||
return NewResponseError(plainM3, plainM4)
|
||||
return newResponseError(plainM3, plainM4)
|
||||
}
|
||||
if plainM4.Error != 0 {
|
||||
return newPairingError(plainM4.Error)
|
||||
}
|
||||
|
||||
// STEP M4. Verify response
|
||||
if !session.VerifyServerAuthenticator(plainM4.Proof) {
|
||||
return errors.New("hap: wrong server auth")
|
||||
if !session.VerifyServerAuthenticator([]byte(plainM4.Proof)) {
|
||||
return errors.New("hap: VerifyServerAuthenticator")
|
||||
}
|
||||
|
||||
// STEP M5. Generate signature
|
||||
@@ -163,12 +158,12 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
// STEP M5. Generate payload
|
||||
plainM5 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{
|
||||
Identifier: c.ClientID,
|
||||
PublicKey: c.ClientPublic(),
|
||||
Signature: signature,
|
||||
PublicKey: string(c.ClientPublic()),
|
||||
Signature: string(signature),
|
||||
}
|
||||
if b, err = tlv8.Marshal(plainM5); err != nil {
|
||||
return
|
||||
@@ -188,10 +183,10 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
|
||||
// STEP M5. Send request
|
||||
cipherM5 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
}{
|
||||
EncryptedData: b,
|
||||
EncryptedData: string(b),
|
||||
State: StateM5,
|
||||
}
|
||||
if res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil {
|
||||
@@ -200,7 +195,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
|
||||
// STEP M6. Read response
|
||||
cipherM6 := struct {
|
||||
EncryptedData []byte `tlv8:"5"`
|
||||
EncryptedData string `tlv8:"5"`
|
||||
State byte `tlv8:"6"`
|
||||
Error byte `tlv8:"7"`
|
||||
}{}
|
||||
@@ -208,19 +203,19 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
return
|
||||
}
|
||||
if cipherM6.State != StateM6 || cipherM6.Error != 0 {
|
||||
return NewResponseError(plainM5, cipherM6)
|
||||
return newResponseError(plainM5, cipherM6)
|
||||
}
|
||||
|
||||
// STEP M6. Decrypt payload
|
||||
b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", cipherM6.EncryptedData)
|
||||
b, err = chacha20poly1305.Decrypt(encryptKey, "PS-Msg06", []byte(cipherM6.EncryptedData))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
plainM6 := struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
Signature []byte `tlv8:"10"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
Signature string `tlv8:"10"`
|
||||
}{}
|
||||
if err = tlv8.Unmarshal(b, &plainM6); err != nil {
|
||||
return
|
||||
@@ -235,15 +230,15 @@ func (c *Client) Pair(mfi bool, pin string) (err error) {
|
||||
}
|
||||
|
||||
b = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey)
|
||||
if !ed25519.ValidateSignature(plainM6.PublicKey, b, plainM6.Signature) {
|
||||
return errors.New("hap: wrong accessory sign")
|
||||
if !ed25519.ValidateSignature([]byte(plainM6.PublicKey), b, []byte(plainM6.Signature)) {
|
||||
return errors.New("hap: ValidateSignature")
|
||||
}
|
||||
|
||||
if c.DeviceID != plainM6.Identifier {
|
||||
return errors.New("hap: wrong DeviceID: " + plainM6.Identifier)
|
||||
}
|
||||
|
||||
c.DevicePublic = plainM6.PublicKey
|
||||
c.DevicePublic = []byte(plainM6.PublicKey)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -264,7 +259,7 @@ func (c *Client) ListPairings() error {
|
||||
// TODO: don't know how to fix array of items
|
||||
var plainM2 struct {
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}
|
||||
@@ -279,13 +274,13 @@ func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) e
|
||||
plainM1 := struct {
|
||||
Method byte `tlv8:"0"`
|
||||
Identifier string `tlv8:"1"`
|
||||
PublicKey []byte `tlv8:"3"`
|
||||
PublicKey string `tlv8:"3"`
|
||||
State byte `tlv8:"6"`
|
||||
Permission byte `tlv8:"11"`
|
||||
}{
|
||||
Method: MethodAddPairing,
|
||||
Identifier: clientID,
|
||||
PublicKey: clientPublic,
|
||||
PublicKey: string(clientPublic),
|
||||
State: StateM1,
|
||||
Permission: PermissionUser,
|
||||
}
|
||||
@@ -330,7 +325,7 @@ func (c *Client) DeletePairing(id string) error {
|
||||
return err
|
||||
}
|
||||
if plainM2.State != StateM2 {
|
||||
return NewResponseError(plainM1, plainM2)
|
||||
return newResponseError(plainM1, plainM2)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+36
-34
@@ -3,12 +3,10 @@ package hap
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,6 +28,12 @@ const (
|
||||
// - 0100b - A problem has been detected on the accessory
|
||||
TXTStatusFlags = "sf" // Status flags (ex. 0, 1)
|
||||
|
||||
StatusPaired = "0"
|
||||
StatusNotPaired = "1"
|
||||
|
||||
CategoryBridge = "2"
|
||||
CategoryCamera = "17"
|
||||
|
||||
StateM1 = 1
|
||||
StateM2 = 2
|
||||
StateM3 = 3
|
||||
@@ -43,28 +47,41 @@ const (
|
||||
MethodAddPairing = 3
|
||||
MethodDeletePairing = 4
|
||||
MethodListPairings = 5
|
||||
)
|
||||
|
||||
const (
|
||||
PermissionUser = 0
|
||||
PermissionAdmin = 1
|
||||
)
|
||||
|
||||
const DeviceAID = 1 // TODO: fix someday
|
||||
|
||||
type JSONAccessories struct {
|
||||
Value []*Accessory `json:"accessories"`
|
||||
}
|
||||
|
||||
type JSONCharacters struct {
|
||||
Value []JSONCharacter `json:"characteristics"`
|
||||
}
|
||||
|
||||
type JSONCharacter struct {
|
||||
AID uint8 `json:"aid"`
|
||||
IID uint64 `json:"iid"`
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
func SanitizePin(pin string) (string, error) {
|
||||
s := strings.ReplaceAll(pin, "-", "")
|
||||
if len(s) != 8 {
|
||||
return "", errors.New("hap: wrong PIN format: " + pin)
|
||||
}
|
||||
// 123-45-678
|
||||
return s[:3] + "-" + s[3:5] + "-" + s[5:], nil
|
||||
}
|
||||
|
||||
func GenerateKey() []byte {
|
||||
_, key, _ := ed25519.GenerateKey(nil)
|
||||
return key
|
||||
}
|
||||
|
||||
func GenerateID(name string) string {
|
||||
sum := sha512.Sum512([]byte(name))
|
||||
return fmt.Sprintf(
|
||||
"%02X:%02X:%02X:%02X:%02X:%02X",
|
||||
sum[0], sum[1], sum[2], sum[3], sum[4], sum[5],
|
||||
)
|
||||
}
|
||||
|
||||
func GenerateUUID() string {
|
||||
//12345678-9012-3456-7890-123456789012
|
||||
data := make([]byte, 16)
|
||||
@@ -87,25 +104,10 @@ func Append(items ...any) (b []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
func NewResponseError(req, res any) error {
|
||||
func newRequestError(req any) error {
|
||||
return fmt.Errorf("hap: wrong request: %#v", req)
|
||||
}
|
||||
|
||||
func newResponseError(req, res any) error {
|
||||
return fmt.Errorf("hap: wrong response: %#v, on request: %#v", res, req)
|
||||
}
|
||||
|
||||
func UnmarshalEvent(res *http.Response) (char *Character, err error) {
|
||||
var data []byte
|
||||
if data, err = io.ReadAll(res.Body); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ch := Characters{}
|
||||
if err = json.Unmarshal(data, &ch); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(ch.Characters) > 1 {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
char = ch.Characters[0]
|
||||
return
|
||||
}
|
||||
|
||||
+27
-35
@@ -85,10 +85,6 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
|
||||
v := value.Uint()
|
||||
return append(b, tag, 1, byte(v)), nil
|
||||
|
||||
case reflect.Int8:
|
||||
v := value.Int()
|
||||
return append(b, tag, 1, byte(v)), nil
|
||||
|
||||
case reflect.Uint16:
|
||||
v := value.Uint()
|
||||
return append(b, tag, 2, byte(v), byte(v>>8)), nil
|
||||
@@ -103,7 +99,13 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
|
||||
|
||||
case reflect.String:
|
||||
v := value.String()
|
||||
b = append(b, tag, byte(len(v)))
|
||||
l := len(v) // support "big" string
|
||||
for ; l > 255; l -= 255 {
|
||||
b = append(b, tag, 255)
|
||||
b = append(b, v[:255]...)
|
||||
v = v[255:]
|
||||
}
|
||||
b = append(b, tag, byte(l))
|
||||
return append(b, v...), nil
|
||||
|
||||
case reflect.Array:
|
||||
@@ -117,19 +119,6 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
// byte array
|
||||
if value.Type().Elem().Kind() == reflect.Uint8 {
|
||||
v := value.Bytes()
|
||||
l := len(v)
|
||||
for ; l > 255; l -= 255 {
|
||||
b = append(b, tag, 255)
|
||||
b = append(b, v[:255]...)
|
||||
v = v[255:]
|
||||
}
|
||||
b = append(b, tag, byte(l))
|
||||
return append(b, v...), nil
|
||||
}
|
||||
|
||||
for i := 0; i < value.Len(); i++ {
|
||||
if i > 0 {
|
||||
b = append(b, 0, 0)
|
||||
@@ -175,24 +164,30 @@ func Unmarshal(data []byte, v any) error {
|
||||
}
|
||||
|
||||
value := reflect.ValueOf(v)
|
||||
kind := value.Type().Kind()
|
||||
kind := value.Kind()
|
||||
|
||||
if kind != reflect.Pointer {
|
||||
return errors.New("tlv8: value should be pointer: " + kind.String())
|
||||
}
|
||||
|
||||
value = value.Elem()
|
||||
kind = value.Type().Kind()
|
||||
kind = value.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Struct:
|
||||
return unmarshalStruct(data, value)
|
||||
if kind == reflect.Interface {
|
||||
value = value.Elem()
|
||||
kind = value.Kind()
|
||||
}
|
||||
|
||||
return errors.New("tlv8: not implemented: " + kind.String())
|
||||
if kind != reflect.Struct {
|
||||
return errors.New("tlv8: not implemented: " + kind.String())
|
||||
}
|
||||
|
||||
return unmarshalStruct(data, value)
|
||||
}
|
||||
|
||||
func unmarshalStruct(b []byte, value reflect.Value) error {
|
||||
var waitSlice bool
|
||||
|
||||
for len(b) >= 2 {
|
||||
t := b[0]
|
||||
l := int(b[1])
|
||||
@@ -200,6 +195,7 @@ func unmarshalStruct(b []byte, value reflect.Value) error {
|
||||
// array item divider
|
||||
if t == 0 && l == 0 {
|
||||
b = b[2:]
|
||||
waitSlice = true
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -228,6 +224,13 @@ func unmarshalStruct(b []byte, value reflect.Value) error {
|
||||
return fmt.Errorf("tlv8: can't find T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
|
||||
}
|
||||
|
||||
if waitSlice {
|
||||
if valueField.Kind() != reflect.Slice {
|
||||
return fmt.Errorf("tlv8: should be slice T=%d,L=%d,V=%x for: %s", t, l, v, value.Type().Name())
|
||||
}
|
||||
waitSlice = false
|
||||
}
|
||||
|
||||
if err := unmarshalValue(v, valueField); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -244,12 +247,6 @@ func unmarshalValue(v []byte, value reflect.Value) error {
|
||||
}
|
||||
value.SetUint(uint64(v[0]))
|
||||
|
||||
case reflect.Int8:
|
||||
if len(v) != 1 {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
}
|
||||
value.SetInt(int64(v[0]))
|
||||
|
||||
case reflect.Uint16:
|
||||
if len(v) != 2 {
|
||||
return errors.New("tlv8: wrong size: " + value.Type().Name())
|
||||
@@ -280,11 +277,6 @@ func unmarshalValue(v []byte, value reflect.Value) error {
|
||||
return nil
|
||||
|
||||
case reflect.Slice:
|
||||
if value.Type().Elem().Kind() == reflect.Uint8 {
|
||||
value.SetBytes(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
i := growSlice(value)
|
||||
return unmarshalValue(v, value.Index(i))
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tlv8
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -36,3 +37,73 @@ func TestMarshal(t *testing.T) {
|
||||
|
||||
require.Equal(t, src, dst)
|
||||
}
|
||||
|
||||
func TestBytes(t *testing.T) {
|
||||
bytes := make([]byte, 255)
|
||||
for i := 0; i < len(bytes); i++ {
|
||||
bytes[i] = byte(i)
|
||||
}
|
||||
|
||||
type Struct struct {
|
||||
String string `tlv8:"1"`
|
||||
}
|
||||
src := Struct{
|
||||
String: string(bytes),
|
||||
}
|
||||
|
||||
b, err := Marshal(src)
|
||||
require.Nil(t, err)
|
||||
|
||||
var dst Struct
|
||||
err = Unmarshal(b, &dst)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, src, dst)
|
||||
require.Equal(t, bytes, []byte(dst.String))
|
||||
}
|
||||
|
||||
func TestVideoCodecParams(t *testing.T) {
|
||||
type VideoCodecParams struct {
|
||||
ProfileID []byte `tlv8:"1"`
|
||||
Level []byte `tlv8:"2"`
|
||||
PacketizationMode byte `tlv8:"3"`
|
||||
CVOEnabled []byte `tlv8:"4"`
|
||||
CVOID []byte `tlv8:"5"`
|
||||
}
|
||||
|
||||
src, err := hex.DecodeString("0101010201000000020102030100040100")
|
||||
require.Nil(t, err)
|
||||
|
||||
var v VideoCodecParams
|
||||
err = Unmarshal(src, &v)
|
||||
require.Nil(t, err)
|
||||
|
||||
dst, err := Marshal(v)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, src, dst)
|
||||
}
|
||||
|
||||
func TestInterface(t *testing.T) {
|
||||
type Struct struct {
|
||||
Byte byte `tlv8:"1"`
|
||||
}
|
||||
|
||||
src := Struct{
|
||||
Byte: 1,
|
||||
}
|
||||
var v1 any = &src
|
||||
|
||||
b, err := Marshal(v1)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, []byte{1, 1, 1}, b)
|
||||
|
||||
var dst Struct
|
||||
var v2 any = &dst
|
||||
|
||||
err = Unmarshal(b, v2)
|
||||
require.Nil(t, err)
|
||||
|
||||
require.Equal(t, src, dst)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user