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/mdns" "github.com/AlexxIT/go2rtc/pkg/srtp" ) type server struct { stream string // stream name from YAML hap *hap.Server // server for HAP connection and encryption mdns *mdns.ServiceEntry srtp *srtp.Server accessory *hap.Accessory // HAP accessory pairings []string // pairings list streams map[string]*homekit.Consumer consumer *homekit.Consumer } func (s *server) UpdateStatus() { // true status is important, or device may be offline in Apple Home if len(s.pairings) == 0 { s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired } else { s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired } } 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, &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, &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)}, } if s.GetPair(conn, id) == nil { s.pairings = append(s.pairings, query.Encode()) s.UpdateStatus() 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.UpdateStatus() s.PatchConfig() break } } func (s *server) PatchConfig() { if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); 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]) }