From f00e64661268006beb75c4e135d0e20289858556 Mon Sep 17 00:00:00 2001 From: Alexey Khit Date: Sat, 2 Sep 2023 06:35:04 +0300 Subject: [PATCH] Add support HomeKit server --- internal/homekit/homekit.go | 147 ++++++++++++++ internal/homekit/server.go | 250 ++++++++++++++++++++++++ pkg/core/core.go | 6 +- pkg/hap/camera/accessory.go | 148 ++++++++++++++ pkg/hap/camera/accessory_test.go | 245 +++++++++++++++++++++++ pkg/hap/camera/ch117_selected_stream.go | 2 +- pkg/hap/camera/stream.go | 18 +- pkg/hap/server.go | 175 +++++++++++++++++ pkg/hap/server_pairing.go | 237 ++++++++++++++++++++++ pkg/homekit/client.go | 15 +- pkg/homekit/consumer.go | 193 ++++++++++++++++++ pkg/homekit/proxy.go | 70 +++++++ pkg/homekit/server.go | 229 ++++++++++++++++++++++ pkg/mdns/README.md | 3 + pkg/mdns/{mdns.go => client.go} | 27 ++- pkg/mdns/server.go | 157 +++++++++++++++ pkg/srtp/server.go | 5 +- pkg/srtp/session.go | 114 ++++++++--- 18 files changed, 1988 insertions(+), 53 deletions(-) create mode 100644 internal/homekit/server.go create mode 100644 pkg/hap/camera/accessory.go create mode 100644 pkg/hap/camera/accessory_test.go create mode 100644 pkg/hap/server.go create mode 100644 pkg/hap/server_pairing.go create mode 100644 pkg/homekit/consumer.go create mode 100644 pkg/homekit/proxy.go create mode 100644 pkg/homekit/server.go create mode 100644 pkg/mdns/README.md rename pkg/mdns/{mdns.go => client.go} (94%) create mode 100644 pkg/mdns/server.go diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index 0e2c60e1..edd67bc1 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -1,6 +1,9 @@ package homekit import ( + "io" + "net" + "net/http" "strings" "github.com/AlexxIT/go2rtc/internal/api" @@ -8,24 +11,168 @@ import ( "github.com/AlexxIT/go2rtc/internal/srtp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/hap/camera" "github.com/AlexxIT/go2rtc/pkg/homekit" + "github.com/AlexxIT/go2rtc/pkg/mdns" "github.com/rs/zerolog" ) func Init() { + var cfg struct { + Mod map[string]struct { + Pin string `json:"pin"` + Name string `json:"name"` + DeviceID string `json:"device_id"` + DevicePrivate string `json:"device_private"` + Pairings []string `json:"pairings"` + //Listen string `json:"listen"` + } `yaml:"homekit"` + } + app.LoadConfig(&cfg) + log = app.GetLogger("homekit") streams.HandleFunc("homekit", streamHandler) api.HandleFunc("api/homekit", apiHandler) + + if cfg.Mod == nil { + return + } + + servers = map[string]*server{} + var entries []*mdns.ServiceEntry + + for id, conf := range cfg.Mod { + stream := streams.Get(id) + if stream == nil { + log.Warn().Msgf("[homekit] missing stream: %s", id) + continue + } + + if conf.Pin == "" { + conf.Pin = "19841984" // default PIN + } + + pin, err := hap.SanitizePin(conf.Pin) + if err != nil { + log.Error().Err(err).Caller().Send() + continue + } + + deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address + name := calcName(conf.Name, deviceID) + + srv := &server{ + stream: id, + srtp: srtp.Server, + pairings: conf.Pairings, + } + + srv.hap = &hap.Server{ + Pin: pin, + DeviceID: deviceID, + DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id), + GetPair: srv.GetPair, + AddPair: srv.AddPair, + Handler: homekit.ServerHandler(srv), + } + + if url := findHomeKitURL(stream); url != "" { + // 1. Act as transparent proxy for HomeKit camera + dial := func() (net.Conn, error) { + client, err := homekit.Dial(url, srtp.Server) + if err != nil { + return nil, err + } + return client.Conn(), nil + } + srv.hap.Handler = homekit.ProxyHandler(srv, dial) + } else { + // 2. Act as basic HomeKit camera + srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version) + srv.hap.Handler = homekit.ServerHandler(srv) + } + + entry := &mdns.ServiceEntry{ + Name: name, + Port: uint16(api.Port()), + Info: map[string]string{ + hap.TXTConfigNumber: "1", + hap.TXTFeatureFlags: "0", + hap.TXTDeviceID: deviceID, + hap.TXTModel: app.UserAgent, + hap.TXTProtoVersion: "1.1", + hap.TXTStateNumber: "1", + hap.TXTStatusFlags: hap.StatusNotPaired, + hap.TXTCategory: hap.CategoryCamera, + hap.TXTSetupHash: srv.hap.SetupHash(), + }, + } + entries = append(entries, entry) + + host := entry.Host(mdns.ServiceHAP) + servers[host] = srv + } + + api.HandleFunc(hap.PathPairSetup, hapPairSetup) + api.HandleFunc(hap.PathPairVerify, hapPairVerify) + + log.Trace().Msgf("[homekit] mnds: %s", entries) + + go func() { + if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil { + log.Error().Err(err).Caller().Send() + } + }() } var log zerolog.Logger +var servers map[string]*server func streamHandler(url string) (core.Producer, error) { return homekit.Dial(url, srtp.Server) } +func hapPairSetup(w http.ResponseWriter, r *http.Request) { + srv, ok := servers[r.Host] + if !ok { + log.Error().Msg("[homekit] unknown host: " + r.Host) + return + } + + conn, rw, err := w.(http.Hijacker).Hijack() + if err != nil { + return + } + + defer conn.Close() + + if err = srv.hap.PairSetup(r, rw, conn); err != nil { + log.Error().Err(err).Caller().Send() + } +} + +func hapPairVerify(w http.ResponseWriter, r *http.Request) { + srv, ok := servers[r.Host] + if !ok { + log.Error().Msg("[homekit] unknown host: " + r.Host) + return + } + + conn, rw, err := w.(http.Hijacker).Hijack() + if err != nil { + return + } + + defer conn.Close() + + if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF { + log.Error().Err(err).Caller().Send() + } +} + func findHomeKitURL(stream *streams.Stream) string { sources := stream.Sources() if len(sources) == 0 { diff --git a/internal/homekit/server.go b/internal/homekit/server.go new file mode 100644 index 00000000..90fc64aa --- /dev/null +++ b/internal/homekit/server.go @@ -0,0 +1,250 @@ +package homekit + +import ( + "crypto/ed25519" + "crypto/sha512" + "encoding/hex" + "fmt" + "net" + "net/url" + "strings" + + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/internal/ffmpeg" + srtp2 "github.com/AlexxIT/go2rtc/internal/srtp" + "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/hap/camera" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" + "github.com/AlexxIT/go2rtc/pkg/homekit" + "github.com/AlexxIT/go2rtc/pkg/magic" + "github.com/AlexxIT/go2rtc/pkg/srtp" +) + +type server struct { + stream string // stream name from YAML + hap *hap.Server // server for HAP connection and encryption + srtp *srtp.Server + accessory *hap.Accessory // HAP accessory + pairings []string // pairings list + + streams map[string]*homekit.Consumer + consumer *homekit.Consumer +} + +func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory { + return []*hap.Accessory{s.accessory} +} + +func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { + log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid) + + char := s.accessory.GetCharacterByID(iid) + if char == nil { + log.Warn().Msgf("[homekit] get unknown characteristic: %d", iid) + return nil + } + + switch char.Type { + case camera.TypeSetupEndpoints: + if s.consumer == nil { + return nil + } + + answer := s.consumer.GetAnswer() + v, err := tlv8.MarshalBase64(answer) + if err != nil { + return nil + } + + return v + } + + return char.Value +} + +func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) { + log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value) + + char := s.accessory.GetCharacterByID(iid) + if char == nil { + log.Warn().Msgf("[homekit] set unknown characteristic: %d", iid) + return + } + + switch char.Type { + case camera.TypeSetupEndpoints: + var offer camera.SetupEndpoints + if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil { + return + } + + s.consumer = homekit.NewConsumer(conn, srtp2.Server) + s.consumer.SetOffer(&offer) + + case camera.TypeSelectedStreamConfiguration: + var conf camera.SelectedStreamConfig + if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil { + return + } + + log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command) + + switch conf.Control.Command { + case camera.SessionCommandEnd: + if consumer := s.streams[conf.Control.SessionID]; consumer != nil { + _ = consumer.Stop() + } + + case camera.SessionCommandStart: + if s.consumer == nil { + return + } + + if !s.consumer.SetConfig(&conf) { + log.Warn().Msgf("[homekit] wrong config") + return + } + + if s.streams == nil { + s.streams = map[string]*homekit.Consumer{} + } + + s.streams[conf.Control.SessionID] = s.consumer + + stream := streams.Get(s.stream) + if err := stream.AddConsumer(s.consumer); err != nil { + return + } + + go func() { + _, _ = s.consumer.WriteTo(nil) + stream.RemoveConsumer(s.consumer) + + delete(s.streams, conf.Control.SessionID) + }() + } + } +} + +func (s *server) GetImage(conn net.Conn, width, height int) []byte { + log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height) + + stream := streams.Get(s.stream) + cons := magic.NewKeyframe() + + if err := stream.AddConsumer(cons); err != nil { + return nil + } + + once := &core.OnceBuffer{} // init and first frame + _, _ = cons.WriteTo(once) + b := once.Buffer() + + stream.RemoveConsumer(cons) + + switch cons.CodecName() { + case core.CodecH264, core.CodecH265: + var err error + if b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil { + return nil + } + } + + return b +} + +func (s *server) GetPair(conn net.Conn, id string) []byte { + log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id) + + for _, pairing := range s.pairings { + if !strings.Contains(pairing, id) { + continue + } + + query, err := url.ParseQuery(pairing) + if err != nil { + continue + } + + if query.Get("client_id") != id { + continue + } + + s := query.Get("client_public") + b, _ := hex.DecodeString(s) + return b + } + return nil +} + +func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) { + log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions) + + query := url.Values{ + "client_id": []string{id}, + "client_public": []string{hex.EncodeToString(public)}, + "permissions": []string{string('0' + permissions)}, + } + s.pairings = append(s.pairings, query.Encode()) + s.PatchConfig() +} + +func (s *server) DelPair(conn net.Conn, id string) { + log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id) + + id = "client_id=" + id + for i, pairing := range s.pairings { + if !strings.Contains(pairing, id) { + continue + } + + s.pairings = append(s.pairings[:i], s.pairings[i+1:]...) + s.PatchConfig() + break + } +} + +func (s *server) PatchConfig() { + if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil { + log.Error().Err(err).Msgf( + "[homekit] can't save %s pairings=%v", s.stream, s.pairings, + ) + } +} + +func calcName(name, seed string) string { + if name != "" { + return name + } + b := sha512.Sum512([]byte(seed)) + return fmt.Sprintf("go2rtc-%02X%02X", b[0], b[2]) +} + +func calcDeviceID(deviceID, seed string) string { + if deviceID != "" { + if len(deviceID) >= 17 { + // 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF) + return deviceID + } + // 2. Use device_id as seed if not zero + seed = deviceID + } + b := sha512.Sum512([]byte(seed)) + return fmt.Sprintf("%02X:%02X:%02X:%02X:%02X:%02X", b[32], b[34], b[36], b[38], b[40], b[42]) +} + +func calcDevicePrivate(private, seed string) []byte { + if private != "" { + // 1. Decode private from HEX string + if b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize { + // 2. Return if OK + return b + } + // 3. Use private as seed if not zero + seed = private + } + b := sha512.Sum512([]byte(seed)) + return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize]) +} diff --git a/pkg/core/core.go b/pkg/core/core.go index 6d7aadef..146533e3 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -110,6 +110,7 @@ const ( type SuperProducer struct { Type string `json:"type,omitempty"` URL string `json:"url,omitempty"` + SDP string `json:"sdp,omitempty"` Medias []*Media `json:"medias,omitempty"` Receivers []*Receiver `json:"receivers,omitempty"` Recv int `json:"recv,omitempty"` @@ -142,9 +143,10 @@ type SuperConsumer struct { URL string `json:"url,omitempty"` RemoteAddr string `json:"remote_addr,omitempty"` UserAgent string `json:"user_agent,omitempty"` + SDP string `json:"sdp,omitempty"` Medias []*Media `json:"medias,omitempty"` - Senders []*Sender `json:"receivers,omitempty"` - Send int `json:"recv,omitempty"` + Senders []*Sender `json:"senders,omitempty"` + Send int `json:"send,omitempty"` } func (s *SuperConsumer) GetMedias() []*Media { diff --git a/pkg/hap/camera/accessory.go b/pkg/hap/camera/accessory.go new file mode 100644 index 00000000..fca77de8 --- /dev/null +++ b/pkg/hap/camera/accessory.go @@ -0,0 +1,148 @@ +package camera + +import ( + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" +) + +func NewAccessory(manuf, model, name, serial, firmware string) *hap.Accessory { + acc := &hap.Accessory{ + AID: hap.DeviceAID, + Services: []*hap.Service{ + hap.ServiceAccessoryInformation(manuf, model, name, serial, firmware), + ServiceCameraRTPStreamManagement(), + //hap.ServiceHAPProtocolInformation(), + //ServiceMicrophone(), + }, + } + acc.InitIID() + return acc +} + +func ServiceMicrophone() *hap.Service { + return &hap.Service{ + Type: "112", // 'Microphone' + Characters: []*hap.Character{ + { + Type: "11A", + Format: hap.FormatBool, + Value: 0, + Perms: hap.EVPRPW, + //Descr: "Mute", + }, + { + Type: "119", + Format: hap.FormatUInt8, + Value: 100, + Perms: hap.EVPRPW, + //Descr: "Volume", + //Unit: hap.UnitPercentage, + //MinValue: 0, + //MaxValue: 100, + //MinStep: 1, + }, + }, + } +} + +func ServiceCameraRTPStreamManagement() *hap.Service { + val120, _ := tlv8.MarshalBase64(StreamingStatus{ + Status: StreamingStatusAvailable, + }) + val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfig{ + Codecs: []VideoCodec{ + { + CodecType: VideoCodecTypeH264, + CodecParams: []VideoParams{ + { + ProfileID: []byte{VideoCodecProfileMain}, + Level: []byte{VideoCodecLevel31, VideoCodecLevel40}, + }, + }, + VideoAttrs: []VideoAttrs{ + {Width: 1920, Height: 1080, Framerate: 30}, + {Width: 1280, Height: 720, Framerate: 30}, // important for iPhones + }, + }, + }, + }) + val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfig{ + Codecs: []AudioCodec{ + { + CodecType: AudioCodecTypeOpus, + CodecParams: []AudioParams{ + { + Channels: 1, + Bitrate: AudioCodecBitrateVariable, + SampleRate: []byte{AudioCodecSampleRate16Khz}, + }, + }, + }, + }, + ComfortNoise: 0, + }) + val116, _ := tlv8.MarshalBase64(SupportedRTPConfig{ + CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, + }) + + service := &hap.Service{ + Type: "110", // 'CameraRTPStreamManagement' + Characters: []*hap.Character{ + { + Type: TypeStreamingStatus, + Format: hap.FormatTLV8, + Value: val120, + Perms: hap.EVPR, + //Descr: "Streaming Status", + }, + { + Type: TypeSupportedVideoStreamConfiguration, + Format: hap.FormatTLV8, + Value: val114, + Perms: hap.PR, + //Descr: "Supported Video Stream Configuration", + }, + { + Type: TypeSupportedAudioStreamConfiguration, + Format: hap.FormatTLV8, + Value: val115, + Perms: hap.PR, + //Descr: "Supported Audio Stream Configuration", + }, + { + Type: TypeSupportedRTPConfiguration, + Format: hap.FormatTLV8, + Value: val116, + Perms: hap.PR, + //Descr: "Supported RTP Configuration", + }, + { + Type: "B0", + Format: hap.FormatUInt8, + Value: 1, + Perms: hap.EVPRPW, + //Descr: "Active", + //MinValue: 0, + //MaxValue: 1, + //MinStep: 1, + //ValidVal: []any{0, 1}, + }, + { + Type: TypeSelectedStreamConfiguration, + Format: hap.FormatTLV8, + Value: "", // important empty + Perms: hap.PRPW, + //Descr: "Selected RTP Stream Configuration", + }, + { + Type: TypeSetupEndpoints, + Format: hap.FormatTLV8, + Value: "", // important empty + Perms: hap.PRPW, + //Descr: "Setup Endpoints", + }, + }, + } + + return service +} diff --git a/pkg/hap/camera/accessory_test.go b/pkg/hap/camera/accessory_test.go new file mode 100644 index 00000000..e9894f0f --- /dev/null +++ b/pkg/hap/camera/accessory_test.go @@ -0,0 +1,245 @@ +package camera + +import ( + "encoding/base64" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/stretchr/testify/require" +) + +type testTLV8 struct { + name string + value string + actual any + expect any + noequal bool +} + +func (test testTLV8) run(t *testing.T) { + if test.actual == nil { + return + } + + src := &hap.Character{Value: test.value, Format: hap.FormatTLV8} + err := src.ReadTLV8(test.actual) + require.Nil(t, err) + + require.Equal(t, test.expect, test.actual) + + dst := &hap.Character{Format: hap.FormatTLV8} + err = dst.Write(test.actual) + require.Nil(t, err) + + a, _ := base64.StdEncoding.DecodeString(test.value) + b, _ := base64.StdEncoding.DecodeString(dst.Value.(string)) + t.Logf("%x\n", a) + t.Logf("%x\n", b) + + if !test.noequal { + require.Equal(t, test.value, dst.Value) + } +} + +func TestAqaraG3(t *testing.T) { + tests := []testTLV8{ + { + name: "120", + value: "AQEA", + actual: &StreamingStatus{}, + expect: &StreamingStatus{ + Status: StreamingStatusAvailable, + }, + }, + { + name: "114", + value: "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", + actual: &SupportedVideoStreamConfig{}, + expect: &SupportedVideoStreamConfig{ + Codecs: []VideoCodec{ + { + CodecType: VideoCodecTypeH264, + CodecParams: []VideoParams{ + { + ProfileID: []byte{VideoCodecProfileMain}, + Level: []byte{VideoCodecLevel31, VideoCodecLevel40}, + CVOEnabled: []byte{0}, + }, + }, + VideoAttrs: []VideoAttrs{ + {Width: 1920, Height: 1080, Framerate: 30}, + {Width: 1280, Height: 720, Framerate: 30}, + {Width: 640, Height: 360, Framerate: 30}, + {Width: 480, Height: 270, Framerate: 30}, + {Width: 320, Height: 180, Framerate: 30}, + {Width: 1280, Height: 960, Framerate: 30}, + {Width: 1024, Height: 768, Framerate: 30}, + {Width: 640, Height: 480, Framerate: 30}, + {Width: 480, Height: 360, Framerate: 30}, + {Width: 320, Height: 240, Framerate: 30}, + }, + }, + }, + }, + }, + { + name: "115", + value: "AQ4BAQICCQEBAQIBAAMBAQIBAA==", + actual: &SupportedAudioStreamConfig{}, + expect: &SupportedAudioStreamConfig{ + Codecs: []AudioCodec{ + { + CodecType: AudioCodecTypeAACELD, + CodecParams: []AudioParams{ + { + Channels: 1, + Bitrate: AudioCodecBitrateVariable, + SampleRate: []byte{AudioCodecSampleRate16Khz}, + }, + }, + }, + }, + ComfortNoise: 0, + }, + }, + { + name: "116", + value: "AgEAAAACAQEAAAIBAg==", + actual: &SupportedRTPConfig{}, + expect: &SupportedRTPConfig{ + CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoNone}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, test.run) + } +} + +func TestHomebridge(t *testing.T) { + tests := []testTLV8{ + { + name: "114", + value: "AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAkABAgK0AAMBHgAAAwsBAkABAgLwAAMBDwAAAwsBAkABAgLwAAMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAoAHAgI4BAMBHgAAAwsBAkAGAgKwBAMBHg==", + actual: &SupportedVideoStreamConfig{}, + expect: &SupportedVideoStreamConfig{ + Codecs: []VideoCodec{ + { + CodecType: VideoCodecTypeH264, + CodecParams: []VideoParams{ + { + ProfileID: []byte{VideoCodecProfileConstrainedBaseline, VideoCodecProfileMain, VideoCodecProfileHigh}, + Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40}, + }, + }, + VideoAttrs: []VideoAttrs{ + + {Width: 320, Height: 180, Framerate: 30}, + {Width: 320, Height: 240, Framerate: 15}, + {Width: 320, Height: 240, Framerate: 30}, + {Width: 480, Height: 270, Framerate: 30}, + {Width: 480, Height: 360, Framerate: 30}, + {Width: 640, Height: 360, Framerate: 30}, + {Width: 640, Height: 480, Framerate: 30}, + {Width: 1280, Height: 720, Framerate: 30}, + {Width: 1280, Height: 960, Framerate: 30}, + {Width: 1920, Height: 1080, Framerate: 30}, + {Width: 1600, Height: 1200, Framerate: 30}, + }, + }, + }, + }, + }, + { + name: "116", + value: "AgEA", + actual: &SupportedRTPConfig{}, + expect: &SupportedRTPConfig{ + CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, test.run) + } +} + +func TestScrypted(t *testing.T) { + tests := []testTLV8{ + { + name: "114", + value: "AVIBAQACEwEBAQIBAAAAAgEBAAACAQIDAQADCwECAA8CAnAIAwEeAAADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECQAECAvAAAwEP", + actual: &SupportedVideoStreamConfig{}, + expect: &SupportedVideoStreamConfig{ + Codecs: []VideoCodec{ + { + CodecType: VideoCodecTypeH264, + CodecParams: []VideoParams{ + { + ProfileID: []byte{VideoCodecProfileMain}, + Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40}, + }, + }, + VideoAttrs: []VideoAttrs{ + {Width: 3840, Height: 2160, Framerate: 30}, + {Width: 1920, Height: 1080, Framerate: 30}, + {Width: 1280, Height: 720, Framerate: 30}, + {Width: 320, Height: 240, Framerate: 15}, + }, + }, + }, + }, + }, + { + name: "115", + value: "AScBAQMCIgEBAQIBAAMBAAAAAwEAAAADAQEAAAMBAQAAAwECAAADAQICAQA=", + actual: &SupportedAudioStreamConfig{}, + expect: &SupportedAudioStreamConfig{ + Codecs: []AudioCodec{ + { + CodecType: AudioCodecTypeOpus, + CodecParams: []AudioParams{ + { + Channels: 1, + Bitrate: AudioCodecBitrateVariable, + SampleRate: []byte{ + AudioCodecSampleRate8Khz, AudioCodecSampleRate8Khz, + AudioCodecSampleRate16Khz, AudioCodecSampleRate16Khz, + AudioCodecSampleRate24Khz, AudioCodecSampleRate24Khz, + }, + }, + }, + }, + }, + ComfortNoise: 0, + }, + }, + { + name: "116", + value: "AgEAAAACAQI=", + actual: &SupportedRTPConfig{}, + expect: &SupportedRTPConfig{ + CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoNone}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, test.run) + } +} + +func TestHass(t *testing.T) { + tests := []testTLV8{ + { + name: "114", + value: "AdABAQACFQMBAAEBAAEBAQEBAgIBAAIBAQIBAgMMAQJAAQICtAADAg8AAwwBAkABAgLwAAMCDwADDAECQAECArQAAwIeAAMMAQJAAQIC8AADAh4AAwwBAuABAgIOAQMCHgADDAEC4AECAmgBAwIeAAMMAQKAAgICaAEDAh4AAwwBAoACAgLgAQMCHgADDAECAAQCAkACAwIeAAMMAQIABAICAAMDAh4AAwwBAgAFAgLQAgMCHgADDAECAAUCAsADAwIeAAMMAQKABwICOAQDAh4A", + }, + { + name: "115", + value: "AQ4BAQMCCQEBAQIBAAMBAgEOAQEDAgkBAQECAQADAQECAQA=", + }, + } + for _, test := range tests { + t.Run(test.name, test.run) + } +} diff --git a/pkg/hap/camera/ch117_selected_stream.go b/pkg/hap/camera/ch117_selected_stream.go index 219a391a..aa0c7038 100644 --- a/pkg/hap/camera/ch117_selected_stream.go +++ b/pkg/hap/camera/ch117_selected_stream.go @@ -26,7 +26,7 @@ type RTPParams struct { PayloadType uint8 `tlv8:"1"` SSRC uint32 `tlv8:"2"` MaxBitrate uint16 `tlv8:"3"` - MinRTCPInterval float32 `tlv8:"4"` + RTCPInterval float32 `tlv8:"4"` MaxMTU []uint16 `tlv8:"5"` ComfortNoisePayloadType []uint8 `tlv8:"6"` } diff --git a/pkg/hap/camera/stream.go b/pkg/hap/camera/stream.go index 87c28f8d..b2ef0d9f 100644 --- a/pkg/hap/camera/stream.go +++ b/pkg/hap/camera/stream.go @@ -32,19 +32,19 @@ func NewStream( videoCodec.RTPParams = []RTPParams{ { - PayloadType: 99, - SSRC: videoSession.Local.SSRC, - MaxBitrate: 299, - MinRTCPInterval: 0.5, - MaxMTU: []uint16{1378}, + PayloadType: 99, + SSRC: videoSession.Local.SSRC, + MaxBitrate: 299, + RTCPInterval: 0.5, + MaxMTU: []uint16{1378}, }, } audioCodec.RTPParams = []RTPParams{ { - PayloadType: 110, - SSRC: audioSession.Local.SSRC, - MaxBitrate: 24, - MinRTCPInterval: 5, + PayloadType: 110, + SSRC: audioSession.Local.SSRC, + MaxBitrate: 24, + RTCPInterval: 5, ComfortNoisePayloadType: []uint8{13}, }, diff --git a/pkg/hap/server.go b/pkg/hap/server.go new file mode 100644 index 00000000..898b076c --- /dev/null +++ b/pkg/hap/server.go @@ -0,0 +1,175 @@ +package hap + +import ( + "bufio" + "crypto/sha512" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + + "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" + "github.com/AlexxIT/go2rtc/pkg/hap/curve25519" + "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" + "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" + "github.com/AlexxIT/go2rtc/pkg/hap/secure" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" +) + +type HandlerFunc func(net.Conn) error + +type Server struct { + Pin string + DeviceID string + DevicePrivate []byte + + GetPair func(conn net.Conn, id string) []byte + AddPair func(conn net.Conn, id string, public []byte, permissions byte) + + Handler HandlerFunc +} + +func (s *Server) ServerPublic() []byte { + return s.DevicePrivate[32:] +} + +//func (s *Server) Status() string { +// if len(s.Pairings) == 0 { +// return StatusNotPaired +// } +// return StatusPaired +//} + +func (s *Server) SetupHash() string { + // should be setup_id (random 4 alphanum) + device_id (mac address) + // but device_id is random, so OK + b := sha512.Sum512([]byte(s.DeviceID)) + return base64.StdEncoding.EncodeToString(b[:4]) +} + +func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { + // Request from iPhone + var plainM1 struct { + PublicKey string `tlv8:"3"` + State byte `tlv8:"6"` + } + if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil { + return err + } + if plainM1.State != StateM1 { + return newRequestError(plainM1) + } + + // Generate the key pair + sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() + sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey)) + if err != nil { + return err + } + + encryptKey, err := hkdf.Sha512( + sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", + ) + if err != nil { + return err + } + + b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey) + signature, err := ed25519.Signature(s.DevicePrivate, b) + if err != nil { + return err + } + + // STEP M2. Response to iPhone + plainM2 := struct { + Identifier string `tlv8:"1"` + Signature string `tlv8:"10"` + }{ + Identifier: s.DeviceID, + Signature: string(signature), + } + if b, err = tlv8.Marshal(plainM2); err != nil { + return err + } + + b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b) + if err != nil { + return err + } + + cipherM2 := struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + EncryptedData string `tlv8:"5"` + }{ + State: StateM2, + PublicKey: string(sessionPublic), + EncryptedData: string(b), + } + body, err := tlv8.Marshal(cipherM2) + if err != nil { + return err + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return err + } + + // STEP M3. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return err + } + + var cipherM3 struct { + EncryptedData string `tlv8:"5"` + State byte `tlv8:"6"` + } + if err = tlv8.UnmarshalReader(req.Body, &cipherM3); err != nil { + return err + } + if cipherM3.State != StateM3 { + return newRequestError(cipherM3) + } + + if b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)); err != nil { + return err + } + + var plainM3 struct { + Identifier string `tlv8:"1"` + Signature string `tlv8:"10"` + } + if err = tlv8.Unmarshal(b, &plainM3); err != nil { + return err + } + + clientPublic := s.GetPair(conn, plainM3.Identifier) + if clientPublic == nil { + return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", plainM3.Identifier) + } + + b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) + if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { + return errors.New("new: ValidateSignature") + } + + // STEP M4. Response to iPhone + payloadM4 := struct { + State byte `tlv8:"6"` + }{ + State: StateM4, + } + if body, err = tlv8.Marshal(payloadM4); err != nil { + return err + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return err + } + + if conn, err = secure.Client(conn, sessionShared, false); err != nil { + return err + } + + return s.Handler(conn) +} diff --git a/pkg/hap/server_pairing.go b/pkg/hap/server_pairing.go new file mode 100644 index 00000000..31d2f626 --- /dev/null +++ b/pkg/hap/server_pairing.go @@ -0,0 +1,237 @@ +package hap + +import ( + "bufio" + "crypto/sha512" + "errors" + "fmt" + "io" + "net" + "net/http" + + "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" + "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" + "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" + "github.com/tadglines/go-pkgs/crypto/srp" +) + +const ( + PairMethodSetup = iota + PairMethodSetupWithAuth + PairMethodVerify + PairMethodAdd + PairMethodRemove + PairMethodList +) + +func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { + if req.Header.Get("Content-Type") != MimeTLV8 { + return errors.New("hap: wrong content type") + } + + // STEP 1. Request from iPhone + var plainM1 struct { + Method byte `tlv8:"0"` + State byte `tlv8:"6"` + Flags uint32 `tlv8:"19"` + } + if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil { + return err + } + if plainM1.State != StateM1 { + return newRequestError(plainM1) + } + + username := []byte("Pair-Setup") + + // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) + pake, err := srp.NewSRP( + "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), + ) + if err != nil { + return err + } + + pake.SaltLength = 16 + + salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin)) + + session := pake.NewServerSession(username, salt, verifier) + + // STEP 2. Response to iPhone + plainM2 := struct { + Salt string `tlv8:"2"` + PublicKey string `tlv8:"3"` + State byte `tlv8:"6"` + }{ + State: StateM2, + PublicKey: string(session.GetB()), + Salt: string(salt), + } + body, err := tlv8.Marshal(plainM2) + if err != nil { + return err + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return err + } + + // STEP 3. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return err + } + + var plainM3 struct { + SessionKey string `tlv8:"3"` + Proof string `tlv8:"4"` + State byte `tlv8:"6"` + } + if err = tlv8.UnmarshalReader(req.Body, &plainM3); err != nil { + return err + } + if plainM3.State != StateM3 { + return newRequestError(plainM3) + } + + // important to compute key before verify client + sessionShared, err := session.ComputeKey([]byte(plainM3.SessionKey)) + if err != nil { + return err + } + + if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) { + return errors.New("hap: VerifyClientAuthenticator") + } + + proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof + + // STEP 4. Response to iPhone + payloadM4 := struct { + Proof string `tlv8:"4"` + State byte `tlv8:"6"` + }{ + Proof: string(proof), + State: StateM4, + } + if body, err = tlv8.Marshal(payloadM4); err != nil { + return err + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return err + } + + // STEP 5. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return err + } + var cipherM5 struct { + EncryptedData string `tlv8:"5"` + State byte `tlv8:"6"` + } + if err = tlv8.UnmarshalReader(req.Body, &cipherM5); err != nil { + return err + } + if cipherM5.State != StateM5 { + return newRequestError(cipherM5) + } + + // decrypt message using session shared + encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") + if err != nil { + return err + } + + b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData)) + if err != nil { + return err + } + + // unpack message from TLV8 + var plainM5 struct { + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` + } + if err = tlv8.Unmarshal(b, &plainM5); err != nil { + return err + } + + // 3. verify client ID and Public + remoteSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", + ) + if err != nil { + return err + } + + b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey) + if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) { + return errors.New("hap: ValidateSignature") + } + + // 4. generate signature to our ID and Public + localSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", + ) + if err != nil { + return err + } + + b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic + signature, err := ed25519.Signature(s.DevicePrivate, b) + if err != nil { + return err + } + + // 5. pack our ID and Public + plainM6 := struct { + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` + }{ + Identifier: s.DeviceID, + PublicKey: string(s.ServerPublic()), + Signature: string(signature), + } + if b, err = tlv8.Marshal(plainM6); err != nil { + return err + } + + // 6. encrypt message + b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b) + if err != nil { + return err + } + + // STEP 6. Response to iPhone + cipherM6 := struct { + EncryptedData string `tlv8:"5"` + State byte `tlv8:"6"` + }{ + State: StateM6, + EncryptedData: string(b), + } + if body, err = tlv8.Marshal(cipherM6); err != nil { + return err + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return err + } + + s.AddPair(conn, plainM5.Identifier, []byte(plainM5.PublicKey), PermissionAdmin) + + return nil +} + +func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error { + header := fmt.Sprintf( + "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", + statusCode, http.StatusText(statusCode), contentType, len(body), + ) + body = append([]byte(header), body...) + if _, err := w.Write(body); err != nil { + return err + } + return w.Flush() +} diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go index 5969da40..0117908a 100644 --- a/pkg/homekit/client.go +++ b/pkg/homekit/client.go @@ -3,6 +3,7 @@ package homekit import ( "encoding/json" "errors" + "fmt" "math/rand" "net" "net/url" @@ -82,6 +83,8 @@ func (c *Client) GetMedias() []*core.Media { return nil } + c.SDP = fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig) + c.Medias = []*core.Media{ videoToMedia(c.videoConfig.Codecs), audioToMedia(c.audioConfig.Codecs), @@ -135,6 +138,10 @@ func (c *Client) Start() error { } } + if c.audioSession.OnReadRTP != nil { + c.audioSession.OnReadRTP = timekeeper(c.audioSession.OnReadRTP) + } + <-deadline.C return nil @@ -151,9 +158,9 @@ func (c *Client) Stop() error { func (c *Client) MarshalJSON() ([]byte, error) { info := &core.Info{ - Type: "HomeKit active producer", - URL: c.hap.URL(), - //SDP: fmt.Sprintf("%+v", *c.config), + Type: "HomeKit active producer", + URL: c.hap.URL(), + SDP: fmt.Sprintf("%+v\n%+v", c.videoConfig, c.audioConfig), Medias: c.Medias, Receivers: c.Receivers, Recv: c.videoSession.Recv + c.audioSession.Recv, @@ -197,7 +204,7 @@ func (c *Client) srtpEndpoint() *srtp.Endpoint { } } -func limitter(handler core.HandlerFunc) core.HandlerFunc { +func timekeeper(handler core.HandlerFunc) core.HandlerFunc { const sampleRate = 16000 const sampleSize = 480 diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go new file mode 100644 index 00000000..88b977b9 --- /dev/null +++ b/pkg/homekit/consumer.go @@ -0,0 +1,193 @@ +package homekit + +import ( + "fmt" + "io" + "math/rand" + "net" + "time" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/h264" + "github.com/AlexxIT/go2rtc/pkg/hap/camera" + "github.com/AlexxIT/go2rtc/pkg/srtp" + "github.com/pion/rtp" +) + +type Consumer struct { + core.SuperConsumer + conn net.Conn + srtp *srtp.Server + + deadline *time.Timer + + sessionID string + videoSession *srtp.Session + audioSession *srtp.Session +} + +func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { + return &Consumer{ + SuperConsumer: core.SuperConsumer{ + Type: "HomeKit passive consumer", + RemoteAddr: conn.RemoteAddr().String(), + Medias: []*core.Media{ + { + Kind: core.KindVideo, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecH264}, + }, + }, + { + Kind: core.KindAudio, + Direction: core.DirectionSendonly, + Codecs: []*core.Codec{ + {Name: core.CodecOpus}, + }, + }, + }, + }, + + conn: conn, + srtp: server, + } +} + +func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) { + c.sessionID = offer.SessionID + c.videoSession = &srtp.Session{ + Remote: &srtp.Endpoint{ + Addr: offer.Address.IPAddr, + Port: offer.Address.VideoRTPPort, + MasterKey: []byte(offer.VideoCrypto.MasterKey), + MasterSalt: []byte(offer.VideoCrypto.MasterSalt), + }, + } + c.audioSession = &srtp.Session{ + Remote: &srtp.Endpoint{ + Addr: offer.Address.IPAddr, + Port: offer.Address.AudioRTPPort, + MasterKey: []byte(offer.AudioCrypto.MasterKey), + MasterSalt: []byte(offer.AudioCrypto.MasterSalt), + }, + } +} + +func (c *Consumer) GetAnswer() *camera.SetupEndpoints { + c.videoSession.Local = c.srtpEndpoint() + c.audioSession.Local = c.srtpEndpoint() + + return &camera.SetupEndpoints{ + SessionID: c.sessionID, + Status: []byte{0}, + Address: camera.Addr{ + IPAddr: c.videoSession.Local.Addr, + VideoRTPPort: c.videoSession.Local.Port, + AudioRTPPort: c.audioSession.Local.Port, + }, + VideoCrypto: camera.CryptoSuite{ + MasterKey: string(c.videoSession.Local.MasterKey), + MasterSalt: string(c.videoSession.Local.MasterSalt), + }, + AudioCrypto: camera.CryptoSuite{ + MasterKey: string(c.audioSession.Local.MasterKey), + MasterSalt: string(c.audioSession.Local.MasterSalt), + }, + VideoSSRC: []uint32{c.videoSession.Local.SSRC}, + AudioSSRC: []uint32{c.audioSession.Local.SSRC}, + } +} + +func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool { + if c.sessionID != conf.Control.SessionID { + return false + } + + c.SDP = fmt.Sprintf("%+v\n%+v", conf.VideoCodec, conf.AudioCodec) + + c.videoSession.Remote.SSRC = conf.VideoCodec.RTPParams[0].SSRC + c.videoSession.PayloadType = conf.VideoCodec.RTPParams[0].PayloadType + c.videoSession.RTCPInterval = toDuration(conf.VideoCodec.RTPParams[0].RTCPInterval) + + c.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC + c.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType + c.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval) + + c.srtp.AddSession(c.videoSession) + c.srtp.AddSession(c.audioSession) + + return true +} + +func (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error { + var session *srtp.Session + if codec.Kind() == core.KindVideo { + session = c.videoSession + } else { + session = c.audioSession + } + + sender := core.NewSender(media, track.Codec) + + if c.deadline == nil { + c.deadline = time.NewTimer(time.Second * 30) + + sender.Handler = func(packet *rtp.Packet) { + c.deadline.Reset(core.ConnDeadline) + if n, err := session.WriteRTP(packet); err == nil { + c.Send += n + } + } + } else { + sender.Handler = func(packet *rtp.Packet) { + if n, err := session.WriteRTP(packet); err == nil { + c.Send += n + } + } + } + + switch codec.Name { + case core.CodecH264: + sender.Handler = h264.RTPPay(1378, sender.Handler) + if track.Codec.IsRTP() { + sender.Handler = h264.RTPDepay(track.Codec, sender.Handler) + } else { + sender.Handler = h264.RepairAVCC(track.Codec, sender.Handler) + } + } + + sender.HandleRTP(track) + c.Senders = append(c.Senders, sender) + return nil +} + +func (c *Consumer) WriteTo(io.Writer) (int64, error) { + if c.deadline != nil { + <-c.deadline.C + } + return 0, nil +} + +func (c *Consumer) Stop() error { + _ = c.SuperConsumer.Close() + if c.deadline != nil { + c.deadline.Reset(0) + } + return c.SuperConsumer.Close() +} + +func (c *Consumer) srtpEndpoint() *srtp.Endpoint { + addr := c.conn.LocalAddr().(*net.TCPAddr) + return &srtp.Endpoint{ + Addr: addr.IP.To4().String(), + Port: uint16(c.srtp.Port()), + MasterKey: []byte(core.RandString(16, 0)), + MasterSalt: []byte(core.RandString(14, 0)), + SSRC: rand.Uint32(), + } +} + +func toDuration(seconds float32) time.Duration { + return time.Duration(seconds * float32(time.Second)) +} diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go new file mode 100644 index 00000000..6c081c23 --- /dev/null +++ b/pkg/homekit/proxy.go @@ -0,0 +1,70 @@ +package homekit + +import ( + "bufio" + "bytes" + "net" + "net/http" + + "github.com/AlexxIT/go2rtc/pkg/hap" +) + +func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc { + return func(controller net.Conn) error { + accessory, err := dial() + if err != nil { + return err + } + + // accessory (ex. Camera) => controller (ex. iPhone) + go proxy(accessory, controller, nil) + + // controller => accessory + return proxy(controller, accessory, pair) + } +} + +func proxy(r, w net.Conn, pair ServerPair) error { + b := make([]byte, 64*1024) + for { + n, err := r.Read(b) + if err != nil { + break + } + + if pair != nil && bytes.HasPrefix(b[:n], []byte("POST /pairings HTTP/1.1")) { + buf := bytes.NewBuffer(b[:n]) + req, err := http.ReadRequest(bufio.NewReader(buf)) + if err != nil { + return err + } + + res, err := handlePairings(r, req, pair) + if err != nil { + return err + } + + buf.Reset() + + if err = res.Write(buf); err != nil { + return err + } + if _, err = buf.WriteTo(r); err != nil { + return err + } + continue + } + + //if n > 512 { + // log.Printf("[hap] %d bytes => %s\n%s...", n, w.RemoteAddr(), b[:512]) + //} else { + // log.Printf("[hap] %d bytes => %s\n%s", n, w.RemoteAddr(), b[:n]) + //} + if _, err = w.Write(b[:n]); err != nil { + break + } + } + _ = r.Close() + _ = w.Close() + return nil +} diff --git a/pkg/homekit/server.go b/pkg/homekit/server.go new file mode 100644 index 00000000..20cfc59d --- /dev/null +++ b/pkg/homekit/server.go @@ -0,0 +1,229 @@ +package homekit + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "strconv" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/hap" + "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" +) + +type Server interface { + ServerPair + ServerAccessory +} + +type ServerPair interface { + GetPair(conn net.Conn, id string) []byte + AddPair(conn net.Conn, id string, public []byte, permissions byte) + DelPair(conn net.Conn, id string) +} + +type ServerAccessory interface { + GetAccessories(conn net.Conn) []*hap.Accessory + GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any + SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) + GetImage(conn net.Conn, width, height int) []byte +} + +func ServerHandler(server Server) hap.HandlerFunc { + return handleRequest(func(conn net.Conn, req *http.Request) (*http.Response, error) { + switch req.URL.Path { + case hap.PathPairings: + return handlePairings(conn, req, server) + + case hap.PathAccessories: + body := hap.JSONAccessories{Value: server.GetAccessories(conn)} + return makeResponse(hap.MimeJSON, body) + + case hap.PathCharacteristics: + switch req.Method { + case "GET": + var v hap.JSONCharacters + + id := req.URL.Query().Get("id") + for _, id = range strings.Split(id, ",") { + s1, s2, _ := strings.Cut(id, ".") + aid, _ := strconv.Atoi(s1) + iid, _ := strconv.ParseUint(s2, 10, 64) + val := server.GetCharacteristic(conn, uint8(aid), iid) + + v.Value = append(v.Value, hap.JSONCharacter{AID: uint8(aid), IID: iid, Value: val}) + } + + return makeResponse(hap.MimeJSON, v) + + case "PUT": + var v struct { + Value []struct { + AID uint8 `json:"aid"` + IID uint64 `json:"iid"` + Value any `json:"value"` + } `json:"characteristics"` + } + if err := json.NewDecoder(req.Body).Decode(&v); err != nil { + return nil, err + } + + for _, char := range v.Value { + server.SetCharacteristic(conn, char.AID, char.IID, char.Value) + } + + res := &http.Response{ + StatusCode: http.StatusNoContent, + Proto: "HTTP", + ProtoMajor: 1, + ProtoMinor: 1, + } + return res, nil + } + + case hap.PathResource: + var v struct { + Width int `json:"image-width"` + Height int `json:"image-height"` + Type string `json:"resource-type"` + } + if err := json.NewDecoder(req.Body).Decode(&v); err != nil { + return nil, err + } + + body := server.GetImage(conn, v.Width, v.Height) + return makeResponse("image/jpeg", body) + } + + return nil, errors.New("hap: unsupported path: " + req.RequestURI) + }) +} + +func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) hap.HandlerFunc { + return func(conn net.Conn) error { + rw := bufio.NewReaderSize(conn, 16*1024) + wr := bufio.NewWriterSize(conn, 16*1024) + for { + req, err := http.ReadRequest(rw) + //debug(req) + if err != nil { + return err + } + + res, err := handle(conn, req) + //debug(res) + if err != nil { + return err + } + + if err = res.Write(wr); err != nil { + return err + } + if err = wr.Flush(); err != nil { + return err + } + } + } +} + +func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Response, error) { + cmd := struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + State byte `tlv8:"6"` + Permissions byte `tlv8:"11"` + }{} + + if err := tlv8.UnmarshalReader(req.Body, &cmd); err != nil { + return nil, err + } + + switch cmd.Method { + case 3: // add + pair.AddPair(conn, cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) + case 4: // delete + pair.DelPair(conn, cmd.Identifier) + } + + body := struct { + State byte `tlv8:"6"` + }{ + State: hap.StateM2, + } + + return makeResponse(hap.MimeTLV8, body) +} + +func makeResponse(mime string, v any) (*http.Response, error) { + var body []byte + var err error + + switch mime { + case hap.MimeJSON: + body, err = json.Marshal(v) + case hap.MimeTLV8: + body, err = tlv8.Marshal(v) + case "image/jpeg": + body = v.([]byte) + } + + if err != nil { + return nil, err + } + + res := &http.Response{ + StatusCode: http.StatusOK, + Proto: "HTTP", + ProtoMajor: 1, + ProtoMinor: 1, + Header: http.Header{ + "Content-Type": []string{mime}, + "Content-Length": []string{strconv.Itoa(len(body))}, + }, + ContentLength: int64(len(body)), + Body: io.NopCloser(bytes.NewReader(body)), + } + return res, nil +} + +//func debug(v any) { +// switch v := v.(type) { +// case *http.Request: +// if v == nil { +// return +// } +// if v.ContentLength != 0 { +// b, err := io.ReadAll(v.Body) +// if err != nil { +// panic(err) +// } +// v.Body = io.NopCloser(bytes.NewReader(b)) +// log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b) +// } else { +// log.Printf("[homekit] request: %s %s ", v.Method, v.RequestURI) +// } +// case *http.Response: +// if v == nil { +// return +// } +// if v.Header.Get("Content-Type") == "image/jpeg" { +// log.Printf("[homekit] response: %d ", v.StatusCode) +// return +// } +// if v.ContentLength != 0 { +// b, err := io.ReadAll(v.Body) +// if err != nil { +// panic(err) +// } +// v.Body = io.NopCloser(bytes.NewReader(b)) +// log.Printf("[homekit] response: %d\n%s", v.StatusCode, b) +// } else { +// log.Printf("[homekit] response: %d ", v.StatusCode) +// } +// } +//} diff --git a/pkg/mdns/README.md b/pkg/mdns/README.md new file mode 100644 index 00000000..e6e21d68 --- /dev/null +++ b/pkg/mdns/README.md @@ -0,0 +1,3 @@ +# Useful links + +- https://grouper.ieee.org/groups/1722/contributions/2009/Bonjour%20Device%20Discovery.pdf \ No newline at end of file diff --git a/pkg/mdns/mdns.go b/pkg/mdns/client.go similarity index 94% rename from pkg/mdns/mdns.go rename to pkg/mdns/client.go index 9441cf60..89673ce9 100644 --- a/pkg/mdns/mdns.go +++ b/pkg/mdns/client.go @@ -30,6 +30,14 @@ func (e *ServiceEntry) String() string { return string(b) } +func (e *ServiceEntry) TXT() []string { + var txt []string + for k, v := range e.Info { + txt = append(txt, k+"="+v) + } + return txt +} + func (e *ServiceEntry) Complete() bool { return e.IP != nil && e.Port > 0 && e.Info != nil } @@ -38,6 +46,21 @@ func (e *ServiceEntry) Addr() string { return fmt.Sprintf("%s:%d", e.IP, e.Port) } +func (e *ServiceEntry) Host(service string) string { + return e.name() + "." + strings.TrimRight(service, ".") +} + +func (e *ServiceEntry) name() string { + b := []byte(e.Name) + for i, c := range b { + if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { + continue + } + b[i] = '-' + } + return string(b) +} + var MulticastAddr = &net.UDPAddr{ IP: net.IP{224, 0, 0, 251}, Port: 5353, @@ -147,8 +170,6 @@ func (b *Browser) ListenMulticastUDP() error { return err } - ctx := context.Background() - // 2. Create senders lc1 := net.ListenConfig{ Control: func(network, address string, c syscall.RawConn) error { @@ -159,6 +180,8 @@ func (b *Browser) ListenMulticastUDP() error { }, } + ctx := context.Background() + for _, ip4 := range ip4s { conn, err := lc1.ListenPacket(ctx, "udp4", ip4.String()+":5353") // same port important if err != nil { diff --git a/pkg/mdns/server.go b/pkg/mdns/server.go new file mode 100644 index 00000000..ec31886e --- /dev/null +++ b/pkg/mdns/server.go @@ -0,0 +1,157 @@ +package mdns + +import ( + "net" + + "github.com/miekg/dns" +) + +// ClassCacheFlush https://datatracker.ietf.org/doc/html/rfc6762#section-10.2 +const ClassCacheFlush = 0x8001 + +func Serve(service string, entries []*ServiceEntry) error { + b := Browser{Service: service} + + if err := b.ListenMulticastUDP(); err != nil { + return err + } + + return b.Serve(entries) +} + +func (b *Browser) Serve(entries []*ServiceEntry) error { + var msg dns.Msg + + buf := make([]byte, 1500) + for { + n, addr, err := b.Recv.ReadFrom(buf) + if err != nil { + break + } + + if err = msg.Unpack(buf[:n]); err != nil { + continue + } + + if !HasQuestionPTP(&msg, b.Service) { + continue + } + + remoteIP := addr.(*net.UDPAddr).IP + localIP := MatchLocalIP(remoteIP) + if localIP == nil { + continue + } + + answer, err := NewDNSAnswer(entries, b.Service, localIP).Pack() + if err != nil { + continue + } + + for _, send := range b.Sends { + _, _ = send.WriteTo(answer, MulticastAddr) + } + } + + return nil +} + +func HasQuestionPTP(msg *dns.Msg, name string) bool { + for _, q := range msg.Question { + if q.Qtype == dns.TypePTR && q.Name == name { + return true + } + } + return false +} + +func NewDNSAnswer(entries []*ServiceEntry, service string, ip net.IP) *dns.Msg { + msg := dns.Msg{ + MsgHdr: dns.MsgHdr{ + Response: true, + Authoritative: true, + }, + } + + for _, entry := range entries { + ptrName := entry.name() + "." + service + srvName := entry.name() + ".local." + + msg.Answer = append( + msg.Answer, + &dns.PTR{ + Hdr: dns.RR_Header{ + Name: service, + Rrtype: dns.TypePTR, + Class: dns.ClassINET, + Ttl: 4500, + }, + Ptr: ptrName, + }, + ) + msg.Extra = append( + msg.Extra, + &dns.TXT{ + Hdr: dns.RR_Header{ + Name: ptrName, + Rrtype: dns.TypeTXT, + Class: ClassCacheFlush, + Ttl: 4500, + }, + Txt: entry.TXT(), + }, + &dns.SRV{ + Hdr: dns.RR_Header{ + Name: ptrName, + Rrtype: dns.TypeSRV, + Class: ClassCacheFlush, + Ttl: 120, + Rdlength: 0, + }, + Port: entry.Port, + Target: srvName, + }, + &dns.A{ + Hdr: dns.RR_Header{ + Name: srvName, + Rrtype: dns.TypeA, + Class: ClassCacheFlush, + Ttl: 120, + Rdlength: 0, + }, + A: ip, + }, + ) + } + + return &msg +} + +func MatchLocalIP(remote net.IP) net.IP { + intfs, err := net.Interfaces() + if err != nil { + return nil + } + + for _, intf := range intfs { + if intf.Flags&net.FlagUp == 0 || intf.Flags&net.FlagLoopback != 0 { + continue + } + + addrs, err := intf.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + switch v := addr.(type) { + case *net.IPNet: + if local := v.IP.To4(); local != nil && v.Contains(remote) { + return local + } + } + } + } + + return nil +} diff --git a/pkg/srtp/server.go b/pkg/srtp/server.go index 3115980e..c1c4b7e8 100644 --- a/pkg/srtp/server.go +++ b/pkg/srtp/server.go @@ -32,10 +32,7 @@ func (s *Server) AddSession(session *Session) { s.mu.Lock() defer s.mu.Unlock() - if err := session.Local.Init(); err != nil { - return - } - if err := session.Remote.Init(); err != nil { + if err := session.init(); err != nil { return } diff --git a/pkg/srtp/session.go b/pkg/srtp/session.go index adc590d5..f70f9df6 100644 --- a/pkg/srtp/session.go +++ b/pkg/srtp/session.go @@ -2,6 +2,7 @@ package srtp import ( "net" + "time" "github.com/pion/rtcp" "github.com/pion/rtp" @@ -18,7 +19,12 @@ type Session struct { Send int // bytes send conn net.PacketConn // local conn endpoint - addr net.Addr // remote addr + + PayloadType uint8 + RTCPInterval time.Duration + + senderRTCP rtcp.SenderReport + senderTime time.Time } type Endpoint struct { @@ -28,35 +34,89 @@ type Endpoint struct { MasterSalt []byte SSRC uint32 + addr net.Addr srtp *srtp.Context } -func (e *Endpoint) Init() error { - var profile srtp.ProtectionProfile +func (e *Endpoint) init() (err error) { + e.addr = &net.UDPAddr{IP: net.ParseIP(e.Addr), Port: int(e.Port)} + e.srtp, err = srtp.CreateContext(e.MasterKey, e.MasterSalt, profile(e.MasterKey)) + return +} - switch len(e.MasterKey) { +func profile(key []byte) srtp.ProtectionProfile { + switch len(key) { case 16: - profile = srtp.ProtectionProfileAes128CmHmacSha1_80 + return srtp.ProtectionProfileAes128CmHmacSha1_80 //case 32: // return srtp.ProtectionProfileAes256CmHmacSha1_80 } + return 0 +} - var err error - e.srtp, err = srtp.CreateContext(e.MasterKey, e.MasterSalt, profile) - return err +func (s *Session) init() error { + if err := s.Local.init(); err != nil { + return err + } + if err := s.Remote.init(); err != nil { + return err + } + + s.senderRTCP.SSRC = s.Local.SSRC + s.senderTime = time.Now().Add(s.RTCPInterval) + + return nil } func (s *Session) WriteRTP(packet *rtp.Packet) (int, error) { - b, err := packet.Marshal() + if s.Local.srtp == nil { + return 0, nil // before init call + } + + if now := time.Now(); now.After(s.senderTime) { + s.senderRTCP.NTPTime = uint64(now.UnixNano()) + s.senderTime = now.Add(s.RTCPInterval) + _, _ = s.WriteRTCP(&s.senderRTCP) + } + + clone := rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: packet.Marker, + PayloadType: s.PayloadType, + SequenceNumber: packet.SequenceNumber, + Timestamp: packet.Timestamp, + SSRC: s.Local.SSRC, + }, + Payload: packet.Payload, + } + + b, err := clone.Marshal() if err != nil { return 0, err } + s.senderRTCP.PacketCount++ + s.senderRTCP.RTPTime = clone.Timestamp + s.senderRTCP.OctetCount += uint32(len(clone.Payload)) + if b, err = s.Local.srtp.EncryptRTP(nil, b, nil); err != nil { return 0, err } - return s.conn.WriteTo(b, s.addr) + return s.conn.WriteTo(b, s.Remote.addr) +} + +func (s *Session) WriteRTCP(packet rtcp.Packet) (int, error) { + b, err := packet.Marshal() + if err != nil { + return 0, err + } + b, err = s.Local.srtp.EncryptRTCP(nil, b, nil) + if err != nil { + return 0, err + } + return s.conn.WriteTo(b, s.Remote.addr) } func (s *Session) ReadRTP(b []byte) { @@ -77,32 +137,24 @@ func (s *Session) ReadRTP(b []byte) { } func (s *Session) ReadRTCP(b []byte) { - header := &rtcp.Header{} - b, err := s.Remote.srtp.DecryptRTCP(nil, b, header) + header := rtcp.Header{} + b, err := s.Remote.srtp.DecryptRTCP(nil, b, &header) if err != nil { return } - if _, err = rtcp.Unmarshal(b); err != nil { + //packets, err := rtcp.Unmarshal(b) + //if err != nil { + // return + //} + //if report, ok := packets[0].(*rtcp.SenderReport); ok { + // log.Printf("[srtp] rtcp type=%d report=%v", header.Type, report) + //} + + if header.Type != rtcp.TypeSenderReport { return } - if header.Type == rtcp.TypeSenderReport { - _ = s.KeepAlive() - } -} - -func (s *Session) KeepAlive() error { - rep := rtcp.ReceiverReport{SSRC: s.Local.SSRC} - b, err := rep.Marshal() - if err != nil { - return err - } - - if b, err = s.Local.srtp.EncryptRTCP(nil, b, nil); err != nil { - return err - } - - _, err = s.conn.WriteTo(b, s.addr) - return err + receiverRTCP := rtcp.ReceiverReport{SSRC: s.Local.SSRC} + _, _ = s.WriteRTCP(&receiverRTCP) }