406 lines
9.3 KiB
Go
406 lines
9.3 KiB
Go
package homekit
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"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/hds"
|
|
"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"
|
|
)
|
|
|
|
type server struct {
|
|
hap *hap.Server // server for HAP connection and encryption
|
|
mdns *mdns.ServiceEntry
|
|
|
|
pairings []string // pairings list
|
|
conns []any
|
|
mu sync.Mutex
|
|
|
|
accessory *hap.Accessory // HAP accessory
|
|
consumer *homekit.Consumer
|
|
proxyURL string
|
|
setupID string
|
|
stream string // stream name from YAML
|
|
}
|
|
|
|
func (s *server) MarshalJSON() ([]byte, error) {
|
|
v := struct {
|
|
Name string `json:"name"`
|
|
DeviceID string `json:"device_id"`
|
|
Paired int `json:"paired,omitempty"`
|
|
CategoryID string `json:"category_id,omitempty"`
|
|
SetupCode string `json:"setup_code,omitempty"`
|
|
SetupID string `json:"setup_id,omitempty"`
|
|
Conns []any `json:"connections,omitempty"`
|
|
}{
|
|
Name: s.mdns.Name,
|
|
DeviceID: s.mdns.Info[hap.TXTDeviceID],
|
|
CategoryID: s.mdns.Info[hap.TXTCategory],
|
|
Paired: len(s.pairings),
|
|
Conns: s.conns,
|
|
}
|
|
if v.Paired == 0 {
|
|
v.SetupCode = s.hap.Pin
|
|
v.SetupID = s.setupID
|
|
}
|
|
return json.Marshal(v)
|
|
}
|
|
|
|
func (s *server) Handle(w http.ResponseWriter, r *http.Request) {
|
|
conn, rw, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
// Fix reading from Body after Hijack.
|
|
r.Body = io.NopCloser(rw)
|
|
|
|
switch r.RequestURI {
|
|
case hap.PathPairSetup:
|
|
id, key, err := s.hap.PairSetup(r, rw)
|
|
if err != nil {
|
|
log.Error().Err(err).Caller().Send()
|
|
return
|
|
}
|
|
|
|
s.AddPair(id, key, hap.PermissionAdmin)
|
|
|
|
case hap.PathPairVerify:
|
|
id, key, err := s.hap.PairVerify(r, rw)
|
|
if err != nil {
|
|
log.Debug().Err(err).Caller().Send()
|
|
return
|
|
}
|
|
|
|
log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr())
|
|
|
|
controller, err := hap.NewConn(conn, rw, key, false)
|
|
if err != nil {
|
|
log.Error().Err(err).Caller().Send()
|
|
return
|
|
}
|
|
|
|
s.AddConn(controller)
|
|
defer s.DelConn(controller)
|
|
|
|
var handler homekit.HandlerFunc
|
|
|
|
switch {
|
|
case s.accessory != nil:
|
|
handler = homekit.ServerHandler(s)
|
|
case s.proxyURL != "":
|
|
client, err := hap.Dial(s.proxyURL)
|
|
if err != nil {
|
|
log.Error().Err(err).Caller().Send()
|
|
return
|
|
}
|
|
handler = homekit.ProxyHandler(s, client.Conn)
|
|
}
|
|
|
|
// If your iPhone goes to sleep, it will be an EOF error.
|
|
if err = handler(controller); err != nil && !errors.Is(err, io.EOF) {
|
|
log.Error().Err(err).Caller().Send()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type logger struct {
|
|
v any
|
|
}
|
|
|
|
func (l logger) String() string {
|
|
switch v := l.v.(type) {
|
|
case *hap.Conn:
|
|
return "hap " + v.RemoteAddr().String()
|
|
case *hds.Conn:
|
|
return "hds " + v.RemoteAddr().String()
|
|
case *homekit.Consumer:
|
|
return "rtp " + v.RemoteAddr
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func (s *server) AddConn(v any) {
|
|
log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v})
|
|
s.mu.Lock()
|
|
s.conns = append(s.conns, v)
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *server) DelConn(v any) {
|
|
log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v})
|
|
s.mu.Lock()
|
|
if i := slices.Index(s.conns, v); i >= 0 {
|
|
s.conns = slices.Delete(s.conns, i, i+1)
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
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) pairIndex(id string) int {
|
|
id = "client_id=" + id
|
|
for i, pairing := range s.pairings {
|
|
if strings.HasPrefix(pairing, id) {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func (s *server) GetPair(id string) []byte {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
if i := s.pairIndex(id); i >= 0 {
|
|
query, _ := url.ParseQuery(s.pairings[i])
|
|
b, _ := hex.DecodeString(query.Get("client_public"))
|
|
return b
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *server) AddPair(id string, public []byte, permissions byte) {
|
|
log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions)
|
|
|
|
s.mu.Lock()
|
|
if s.pairIndex(id) < 0 {
|
|
s.pairings = append(s.pairings, fmt.Sprintf(
|
|
"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
|
|
))
|
|
s.UpdateStatus()
|
|
s.PatchConfig()
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
func (s *server) DelPair(id string) {
|
|
log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id)
|
|
|
|
s.mu.Lock()
|
|
if i := s.pairIndex(id); i >= 0 {
|
|
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
|
|
s.UpdateStatus()
|
|
s.PatchConfig()
|
|
}
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
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 (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().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", 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:
|
|
consumer := s.consumer
|
|
if consumer == nil {
|
|
return nil
|
|
}
|
|
|
|
answer := 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().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", 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.SetupEndpointsRequest
|
|
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
|
|
return
|
|
}
|
|
|
|
consumer := homekit.NewConsumer(conn, srtp2.Server)
|
|
consumer.SetOffer(&offer)
|
|
s.consumer = consumer
|
|
|
|
case camera.TypeSelectedStreamConfiguration:
|
|
var conf camera.SelectedStreamConfiguration
|
|
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
|
|
return
|
|
}
|
|
|
|
log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)
|
|
|
|
switch conf.Control.Command {
|
|
case camera.SessionCommandEnd:
|
|
for _, consumer := range s.conns {
|
|
if consumer, ok := consumer.(*homekit.Consumer); ok {
|
|
if consumer.SessionID() == conf.Control.SessionID {
|
|
_ = consumer.Stop()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
case camera.SessionCommandStart:
|
|
consumer := s.consumer
|
|
if consumer == nil {
|
|
return
|
|
}
|
|
|
|
if !consumer.SetConfig(&conf) {
|
|
log.Warn().Msgf("[homekit] wrong config")
|
|
return
|
|
}
|
|
|
|
s.AddConn(consumer)
|
|
|
|
stream := streams.Get(s.stream)
|
|
if err := stream.AddConsumer(consumer); err != nil {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
_, _ = consumer.WriteTo(nil)
|
|
stream.RemoveConsumer(consumer)
|
|
|
|
s.DelConn(consumer)
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
|
|
log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", 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 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])
|
|
}
|
|
|
|
func calcSetupID(seed string) string {
|
|
b := sha512.Sum512([]byte(seed))
|
|
return fmt.Sprintf("%02X%02X", b[44], b[46])
|
|
}
|
|
|
|
func calcCategoryID(categoryID string) string {
|
|
switch categoryID {
|
|
case "bridge":
|
|
return hap.CategoryBridge
|
|
case "doorbell":
|
|
return hap.CategoryDoorbell
|
|
}
|
|
if core.Atoi(categoryID) > 0 {
|
|
return categoryID
|
|
}
|
|
return hap.CategoryCamera
|
|
}
|