From 158f9d3a08ebb17f96bc47f28cfb27dc56a26807 Mon Sep 17 00:00:00 2001 From: Alex X Date: Sun, 9 Nov 2025 21:58:44 +0300 Subject: [PATCH] Code refactoring for HomeKit server --- internal/homekit/api.go | 105 +++++---- internal/homekit/homekit.go | 76 ++----- internal/homekit/server.go | 299 ++++++++++++++++-------- pkg/hap/client.go | 10 +- pkg/hap/client_pairing.go | 5 +- pkg/hap/{secure/secure.go => conn.go} | 47 +++- pkg/hap/hds/hds.go | 33 ++- pkg/hap/server.go | 314 ++++++++++++++++++++++---- pkg/hap/server_pairing.go | 266 ---------------------- pkg/homekit/consumer.go | 6 +- pkg/homekit/log/debug.go | 45 ++++ pkg/homekit/producer.go | 16 +- pkg/homekit/proxy.go | 54 +++-- pkg/homekit/server.go | 57 +---- www/add.html | 15 +- 15 files changed, 742 insertions(+), 606 deletions(-) rename pkg/hap/{secure/secure.go => conn.go} (74%) delete mode 100644 pkg/hap/server_pairing.go create mode 100644 pkg/homekit/log/debug.go diff --git a/internal/homekit/api.go b/internal/homekit/api.go index 9f76c2d6..5d2b38d2 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -3,6 +3,7 @@ package homekit import ( "errors" "fmt" + "io" "net/http" "net/url" "strings" @@ -14,56 +15,84 @@ import ( "github.com/AlexxIT/go2rtc/pkg/mdns" ) -func apiHandler(w http.ResponseWriter, r *http.Request) { +func apiDiscovery(w http.ResponseWriter, r *http.Request) { + sources, err := discovery() + if err != nil { + api.Error(w, err) + return + } + + urls := findHomeKitURLs() + for id, u := range urls { + deviceID := u.Query().Get("device_id") + for _, source := range sources { + if strings.Contains(source.URL, deviceID) { + source.Location = id + break + } + } + } + + for _, source := range sources { + if source.Location == "" { + source.Location = " " + } + } + + api.ResponseSources(w, sources) +} + +func apiHomekit(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch r.Method { case "GET": - sources, err := discovery() - if err != nil { - api.Error(w, err) - return + if id := r.Form.Get("id"); id != "" { + api.ResponsePrettyJSON(w, servers[id]) + } else { + api.ResponsePrettyJSON(w, servers) } - urls := findHomeKitURLs() - for id, u := range urls { - deviceID := u.Query().Get("device_id") - for _, source := range sources { - if strings.Contains(source.URL, deviceID) { - source.Location = id - break - } - } - } - - for _, source := range sources { - if source.Location == "" { - source.Location = " " - } - } - - api.ResponseSources(w, sources) - case "POST": - if err := r.ParseMultipartForm(1024); err != nil { - api.Error(w, err) - return - } - - if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil { - api.Error(w, err) + id := r.Form.Get("id") + rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin") + if err := apiPair(id, rawURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } case "DELETE": - if err := r.ParseMultipartForm(1024); err != nil { - api.Error(w, err) - return - } - - if err := apiUnpair(r.Form.Get("id")); err != nil { - api.Error(w, err) + id := r.Form.Get("id") + if err := apiUnpair(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } } } +func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) { + src := r.URL.Query().Get("src") + stream := streams.Get(src) + rawURL := findHomeKitURL(stream.Sources()) + + client, err := hap.Dial(rawURL) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer client.Close() + + res, err := client.Get(hap.PathAccessories) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", api.MimeJSON) + _, _ = io.Copy(w, res.Body) +} + func discovery() ([]*api.Source, error) { var sources []*api.Source diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index b4237211..a6b02bf2 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -2,8 +2,6 @@ package homekit import ( "errors" - "io" - "net" "net/http" "strings" @@ -35,12 +33,15 @@ func Init() { streams.HandleFunc("homekit", streamHandler) - api.HandleFunc("api/homekit", apiHandler) + api.HandleFunc("api/homekit", apiHomekit) + api.HandleFunc("api/homekit/accessories", apiHomekitAccessories) + api.HandleFunc("api/discovery/homekit", apiDiscovery) if cfg.Mod == nil { return } + hosts = map[string]*server{} servers = map[string]*server{} var entries []*mdns.ServiceEntry @@ -66,33 +67,14 @@ func Init() { 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.Sources()); 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) + Pin: pin, + DeviceID: deviceID, + DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id), + GetClientPublic: srv.GetPair, } srv.mdns = &mdns.ServiceEntry{ @@ -114,15 +96,24 @@ func Init() { srv.UpdateStatus() + if url := findHomeKitURL(stream.Sources()); url != "" { + // 1. Act as transparent proxy for HomeKit camera + srv.proxyURL = url + } else { + // 2. Act as basic HomeKit camera + srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version) + } + host := srv.mdns.Host(mdns.ServiceHAP) - servers[host] = srv + hosts[host] = srv + servers[id] = srv + + log.Trace().Msgf("[homekit] new server: %s", srv.mdns) } api.HandleFunc(hap.PathPairSetup, hapHandler) api.HandleFunc(hap.PathPairVerify, hapHandler) - log.Trace().Msgf("[homekit] mdns: %s", entries) - go func() { if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil { log.Error().Err(err).Caller().Send() @@ -131,6 +122,7 @@ func Init() { } var log zerolog.Logger +var hosts map[string]*server var servers map[string]*server func streamHandler(rawURL string) (core.Producer, error) { @@ -149,45 +141,27 @@ func streamHandler(rawURL string) (core.Producer, error) { } func resolve(host string) *server { - if len(servers) == 1 { - for _, srv := range servers { + if len(hosts) == 1 { + for _, srv := range hosts { return srv } } - if srv, ok := servers[host]; ok { + if srv, ok := hosts[host]; ok { return srv } return nil } func hapHandler(w http.ResponseWriter, r *http.Request) { - conn, rw, err := w.(http.Hijacker).Hijack() - if err != nil { - return - } - - defer conn.Close() - // Can support multiple HomeKit cameras on single port ONLY for Apple devices. // Doesn't support Home Assistant and any other open source projects // because they don't send the host header in requests. srv := resolve(r.Host) if srv == nil { log.Error().Msg("[homekit] unknown host: " + r.Host) - _ = hap.WriteBackoff(rw) return } - - switch r.RequestURI { - case hap.PathPairSetup: - err = srv.hap.PairSetup(r, rw, conn) - case hap.PathPairVerify: - err = srv.hap.PairVerify(r, rw, conn) - } - - if err != nil && err != io.EOF { - log.Error().Err(err).Caller().Send() - } + srv.Handle(w, r) } func findHomeKitURL(sources []string) string { diff --git a/internal/homekit/server.go b/internal/homekit/server.go index d4d81456..57e97287 100644 --- a/internal/homekit/server.go +++ b/internal/homekit/server.go @@ -4,10 +4,16 @@ 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" @@ -16,23 +22,133 @@ import ( "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" - "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 + hap *hap.Server // server for HAP connection and encryption + mdns *mdns.ServiceEntry - streams map[string]*homekit.Consumer - consumer *homekit.Consumer + pairings []string // pairings list + conns []any + mu sync.Mutex + + accessory *hap.Accessory // HAP accessory + consumer *homekit.Consumer + proxyURL 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"` + Conns []any `json:"connections"` + }{ + Name: s.mdns.Name, + DeviceID: s.mdns.Info[hap.TXTDeviceID], + Paired: len(s.pairings), + Conns: s.conns, + } + 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() { @@ -44,12 +160,68 @@ func (s *server) UpdateStatus() { } } +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().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid) + 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 { @@ -59,11 +231,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { switch char.Type { case camera.TypeSetupEndpoints: - if s.consumer == nil { + consumer := s.consumer + if consumer == nil { return nil } - answer := s.consumer.GetAnswer() + answer := consumer.GetAnswer() v, err := tlv8.MarshalBase64(answer) if err != nil { return nil @@ -76,7 +249,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { } 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) + 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 { @@ -91,8 +264,9 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a return } - s.consumer = homekit.NewConsumer(conn, srtp2.Server) - s.consumer.SetOffer(&offer) + consumer := homekit.NewConsumer(conn, srtp2.Server) + consumer.SetOffer(&offer) + s.consumer = consumer case camera.TypeSelectedStreamConfiguration: var conf camera.SelectedStreamConfiguration @@ -100,47 +274,49 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a return } - log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command) + 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: - if consumer := s.streams[conf.Control.SessionID]; consumer != nil { - _ = consumer.Stop() + for _, consumer := range s.conns { + if consumer, ok := consumer.(*homekit.Consumer); ok { + if consumer.SessionID() == conf.Control.SessionID { + _ = consumer.Stop() + return + } + } } case camera.SessionCommandStart: - if s.consumer == nil { + consumer := s.consumer + if consumer == nil { return } - if !s.consumer.SetConfig(&conf) { + if !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 + s.AddConn(consumer) stream := streams.Get(s.stream) - if err := stream.AddConsumer(s.consumer); err != nil { + if err := stream.AddConsumer(consumer); err != nil { return } go func() { - _, _ = s.consumer.WriteTo(nil) - stream.RemoveConsumer(s.consumer) + _, _ = consumer.WriteTo(nil) + stream.RemoveConsumer(consumer) - delete(s.streams, conf.Control.SessionID) + s.DelConn(consumer) }() } } } 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) + log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height) stream := streams.Get(s.stream) cons := magic.NewKeyframe() @@ -166,69 +342,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte { 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 diff --git a/pkg/hap/client.go b/pkg/hap/client.go index bde85277..ed4faa02 100644 --- a/pkg/hap/client.go +++ b/pkg/hap/client.go @@ -18,7 +18,6 @@ import ( "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" "github.com/AlexxIT/go2rtc/pkg/mdns" ) @@ -46,7 +45,7 @@ type Client struct { err error } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -61,6 +60,10 @@ func NewClient(rawURL string) (*Client, error) { ClientPrivate: DecodeKey(query.Get("client_private")), } + if err = c.Dial(); err != nil { + return nil, err + } + return c, nil } @@ -96,6 +99,7 @@ func (c *Client) Dial() (err error) { return false }) + // TODO: close conn on error if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil { return } @@ -219,7 +223,7 @@ func (c *Client) Dial() (err error) { rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn)) // like tls.Client wrapper over net.Conn - if c.Conn, err = secure.Client(c.Conn, rw, sessionShared, true); err != nil { + if c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil { return } // new reader for new conn diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go index a58526d9..f253783d 100644 --- a/pkg/hap/client_pairing.go +++ b/pkg/hap/client_pairing.go @@ -121,9 +121,7 @@ func (c *Client) Pair(feature, pin string) (err error) { username := []byte("Pair-Setup") // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) - pake, err := srp.NewSRP( - "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), - ) + pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username)) if err != nil { return } @@ -132,6 +130,7 @@ func (c *Client) Pair(feature, pin string) (err error) { // username: "Pair-Setup", password: PIN (with dashes) session := pake.NewClientSession(username, []byte(pin)) + sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey)) if err != nil { return diff --git a/pkg/hap/secure/secure.go b/pkg/hap/conn.go similarity index 74% rename from pkg/hap/secure/secure.go rename to pkg/hap/conn.go index a42c7dea..2b039dc8 100644 --- a/pkg/hap/secure/secure.go +++ b/pkg/hap/conn.go @@ -1,13 +1,16 @@ -package secure +package hap import ( "bufio" "encoding/binary" + "encoding/json" "errors" "io" "net" + "sync" "time" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" ) @@ -15,16 +18,33 @@ import ( type Conn struct { conn net.Conn rw *bufio.ReadWriter + wmu sync.Mutex encryptKey []byte decryptKey []byte encryptCnt uint64 decryptCnt uint64 + //ClientID string SharedKey []byte + + recv int + send int } -func Client(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) { +func (c *Conn) MarshalJSON() ([]byte, error) { + conn := core.Connection{ + ID: core.ID(c), + FormatName: "homekit", + Protocol: "hap", + RemoteAddr: c.conn.RemoteAddr().String(), + Recv: c.recv, + Send: c.send, + } + return json.Marshal(conn) +} + +func NewConn(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) { key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key") if err != nil { return nil, err @@ -52,8 +72,8 @@ func Client(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool } const ( - // PacketSizeMax is the max length of encrypted packets - PacketSizeMax = 0x400 + // packetSizeMax is the max length of encrypted packets + packetSizeMax = 0x400 VerifySize = 2 NonceSize = 8 @@ -61,18 +81,18 @@ const ( ) func (c *Conn) Read(b []byte) (n int, err error) { - if cap(b) < PacketSizeMax { + if cap(b) < packetSizeMax { return 0, errors.New("hap: read buffer is too small") } - verify := make([]byte, 2) // verify = plain message size + verify := make([]byte, VerifySize) // verify = plain message size if _, err = io.ReadFull(c.rw, verify); err != nil { return } n = int(binary.LittleEndian.Uint16(verify)) - ciphertext := make([]byte, n+Overhead) + ciphertext := make([]byte, n+Overhead) if _, err = io.ReadFull(c.rw, ciphertext); err != nil { return } @@ -82,18 +102,23 @@ func (c *Conn) Read(b []byte) (n int, err error) { c.decryptCnt++ _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify) + + c.recv += n return } func (c *Conn) Write(b []byte) (n int, err error) { - buf := make([]byte, 0, PacketSizeMax+Overhead) + c.wmu.Lock() + defer c.wmu.Unlock() + + buf := make([]byte, 0, packetSizeMax+Overhead) nonce := make([]byte, NonceSize) verify := make([]byte, VerifySize) for len(b) > 0 { size := len(b) - if size > PacketSizeMax { - size = PacketSizeMax + if size > packetSizeMax { + size = packetSizeMax } binary.LittleEndian.PutUint16(verify, uint16(size)) @@ -118,6 +143,8 @@ func (c *Conn) Write(b []byte) (n int, err error) { } err = c.rw.Flush() + + c.send += n return } diff --git a/pkg/hap/hds/hds.go b/pkg/hap/hds/hds.go index a7b2c74a..60ee05d2 100644 --- a/pkg/hap/hds/hds.go +++ b/pkg/hap/hds/hds.go @@ -4,16 +4,18 @@ package hds import ( "bufio" "encoding/binary" + "encoding/json" "io" "net" "time" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" ) -func Client(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { +func NewConn(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key") if err != nil { return nil, err @@ -49,6 +51,21 @@ type Conn struct { encryptKey []byte decryptCnt uint64 encryptCnt uint64 + + recv int + send int +} + +func (c *Conn) MarshalJSON() ([]byte, error) { + conn := core.Connection{ + ID: core.ID(c), + FormatName: "homekit", + Protocol: "hds", + RemoteAddr: c.conn.RemoteAddr().String(), + Recv: c.recv, + Send: c.send, + } + return json.Marshal(conn) } func (c *Conn) Read(p []byte) (n int, err error) { @@ -59,16 +76,18 @@ func (c *Conn) Read(p []byte) (n int, err error) { n = int(binary.BigEndian.Uint32(verify) & 0xFFFFFF) - ciphertext := make([]byte, n+secure.Overhead) + ciphertext := make([]byte, n+hap.Overhead) if _, err = io.ReadFull(c.rd, ciphertext); err != nil { return } - nonce := make([]byte, secure.NonceSize) + nonce := make([]byte, hap.NonceSize) binary.LittleEndian.PutUint64(nonce, c.decryptCnt) c.decryptCnt++ _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, p[:0], nonce, ciphertext, verify) + + c.recv += n return } @@ -81,11 +100,11 @@ func (c *Conn) Write(b []byte) (n int, err error) { return } - nonce := make([]byte, secure.NonceSize) + nonce := make([]byte, hap.NonceSize) binary.LittleEndian.PutUint64(nonce, c.encryptCnt) c.encryptCnt++ - buf := make([]byte, n+secure.Overhead) + buf := make([]byte, n+hap.Overhead) if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil { return } @@ -95,6 +114,8 @@ func (c *Conn) Write(b []byte) (n int, err error) { } err = c.wr.Flush() + + c.send += n return } diff --git a/pkg/hap/server.go b/pkg/hap/server.go index 99c86f6b..f962a440 100644 --- a/pkg/hap/server.go +++ b/pkg/hap/server.go @@ -6,28 +6,23 @@ import ( "encoding/base64" "errors" "fmt" - "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" + "github.com/tadglines/go-pkgs/crypto/srp" ) -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 + // GetClientPublic may be nil, so client validation will be disabled + GetClientPublic func(id string) []byte } func (s *Server) ServerPublic() []byte { @@ -48,37 +43,240 @@ func (s *Server) SetupHash() string { return base64.StdEncoding.EncodeToString(b[:4]) } -func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - // Request from iPhone +func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) { + // STEP 1. Request from iPhone var plainM1 struct { - PublicKey string `tlv8:"3"` - State byte `tlv8:"6"` + State byte `tlv8:"6"` + Method byte `tlv8:"0"` + Flags uint32 `tlv8:"19"` } - if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { - return err + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { + return } if plainM1.State != StateM1 { - return newRequestError(plainM1) + err = newRequestError(plainM1) + return + } + + 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 + } + + pake.SaltLength = 16 + + salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin)) + if err != nil { + return + } + + session := pake.NewServerSession(username, salt, verifier) + + // STEP 2. Response to iPhone + plainM2 := struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + Salt string `tlv8:"2"` + }{ + State: StateM2, + PublicKey: string(session.GetB()), + Salt: string(salt), + } + body, err := tlv8.Marshal(plainM2) + if err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + // STEP 3. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return + } + + var plainM3 struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + Proof string `tlv8:"4"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil { + return + } + if plainM3.State != StateM3 { + err = newRequestError(plainM3) + return + } + + // important to compute key before verify client + sessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey)) + if err != nil { + return + } + + if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) { + err = errors.New("hap: VerifyClientAuthenticator") + return + } + + proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof + + // STEP 4. Response to iPhone + payloadM4 := struct { + State byte `tlv8:"6"` + Proof string `tlv8:"4"` + }{ + State: StateM4, + Proof: string(proof), + } + if body, err = tlv8.Marshal(payloadM4); err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + // STEP 5. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return + } + var cipherM5 struct { + State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil { + return + } + if cipherM5.State != StateM5 { + err = newRequestError(cipherM5) + return + } + + // decrypt message using session shared + encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") + if err != nil { + return + } + + b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData)) + if err != nil { + return + } + + // 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 + } + + // 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 + } + + b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey) + if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) { + err = errors.New("hap: ValidateSignature") + return + } + + // 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 + } + + b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic + signature, err := ed25519.Signature(s.DevicePrivate, b) + if err != nil { + return + } + + // 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 + } + + // 6. encrypt message + b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b) + if err != nil { + return + } + + // STEP 6. Response to iPhone + cipherM6 := struct { + State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` + }{ + State: StateM6, + EncryptedData: string(b), + } + if body, err = tlv8.Marshal(cipherM6); err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + id = plainM5.Identifier + publicKey = []byte(plainM5.PublicKey) + + return +} + +func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) { + // Request from iPhone + var plainM1 struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { + return + } + if plainM1.State != StateM1 { + err = newRequestError(plainM1) + return } // Generate the key pair sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey)) if err != nil { - return err + return } encryptKey, err := hkdf.Sha512( sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", ) if err != nil { - return err + return } b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey) signature, err := ed25519.Signature(s.DevicePrivate, b) if err != nil { - return err + return } // STEP M2. Response to iPhone @@ -90,12 +288,12 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co Signature: string(signature), } if b, err = tlv8.Marshal(plainM2); err != nil { - return err + return } b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b) if err != nil { - return err + return } cipherM2 := struct { @@ -109,30 +307,32 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co } body, err := tlv8.Marshal(cipherM2) if err != nil { - return err + return } if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err + return } // STEP M3. Request from iPhone if req, err = http.ReadRequest(rw.Reader); err != nil { - return err + return } var cipherM3 struct { - EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` } if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil { - return err + return } if cipherM3.State != StateM3 { - return newRequestError(cipherM3) + err = newRequestError(cipherM3) + return } - if b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)); err != nil { - return err + b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)) + if err != nil { + return } var plainM3 struct { @@ -140,17 +340,21 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co Signature string `tlv8:"10"` } if err = tlv8.Unmarshal(b, &plainM3); err != nil { - return err + return } - clientPublic := s.GetPair(conn, plainM3.Identifier) - if clientPublic == nil { - return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier) - } + if s.GetClientPublic != nil { + clientPublic := s.GetClientPublic(plainM3.Identifier) + if clientPublic == nil { + err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier) + return + } - b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) - if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { - return errors.New("new: ValidateSignature") + b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) + if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { + err = errors.New("hap: ValidateSignature") + return + } } // STEP M4. Response to iPhone @@ -160,15 +364,41 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co State: StateM4, } if body, err = tlv8.Marshal(payloadM4); err != nil { - return err + return } if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err + return } - if conn, err = secure.Client(conn, rw, sessionShared, false); err != nil { - return err - } + id = plainM3.Identifier + sessionKey = sessionShared - return s.Handler(conn) + return } + +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() +} + +//func WriteBackoff(rw *bufio.ReadWriter) error { +// plainM2 := struct { +// State byte `tlv8:"6"` +// Error byte `tlv8:"7"` +// }{ +// State: StateM2, +// Error: 3, // BackoffError +// } +// body, err := tlv8.Marshal(plainM2) +// if err != nil { +// return err +// } +// return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) +//} diff --git a/pkg/hap/server_pairing.go b/pkg/hap/server_pairing.go deleted file mode 100644 index 571ba7a2..00000000 --- a/pkg/hap/server_pairing.go +++ /dev/null @@ -1,266 +0,0 @@ -package hap - -import ( - "bufio" - "crypto/sha512" - "errors" - "fmt" - "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) HandleConn(conn net.Conn) error { - rd := bufio.NewReader(conn) - req, err := http.ReadRequest(rd) - if err != nil { - return err - } - - rw := bufio.NewReadWriter(rd, bufio.NewWriter(conn)) - - switch req.RequestURI { - case PathPairSetup: - return s.PairSetup(req, rw, conn) - case PathPairVerify: - return s.PairVerify(req, rw, conn) - } - - return errors.New("hap: unsupported request uri: " + req.RequestURI) -} - -func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - // STEP 1. Request from iPhone - var plainM1 struct { - Method byte `tlv8:"0"` - State byte `tlv8:"6"` - Flags uint32 `tlv8:"19"` - } - if err := tlv8.UnmarshalReader(req.Body, 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, req.ContentLength, &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, req.ContentLength, &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() -} - -func WriteBackoff(rw *bufio.ReadWriter) error { - plainM2 := struct { - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - }{ - State: StateM2, - Error: 3, // BackoffError - } - body, err := tlv8.Marshal(plainM2) - if err != nil { - return err - } - return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) -} diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index 05204218..c1be7447 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -49,7 +49,7 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { Connection: core.Connection{ ID: core.NewID(), FormatName: "homekit", - Protocol: "udp", + Protocol: "rtp", RemoteAddr: conn.RemoteAddr().String(), Medias: medias, Transport: conn, @@ -59,6 +59,10 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { } } +func (c *Consumer) SessionID() string { + return c.sessionID +} + func (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) { c.sessionID = offer.SessionID c.videoSession = &srtp.Session{ diff --git a/pkg/homekit/log/debug.go b/pkg/homekit/log/debug.go new file mode 100644 index 00000000..1fb60be2 --- /dev/null +++ b/pkg/homekit/log/debug.go @@ -0,0 +1,45 @@ +package log + +import ( + "bytes" + "io" + "log" + "net/http" +) + +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: %s %d\n%s", v.Proto, v.StatusCode, b) + } else { + log.Printf("[homekit] response: %s %d ", v.Proto, v.StatusCode) + } + } +} diff --git a/pkg/homekit/producer.go b/pkg/homekit/producer.go index 3351a736..04719612 100644 --- a/pkg/homekit/producer.go +++ b/pkg/homekit/producer.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "net" - "net/url" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -34,24 +33,11 @@ type Client struct { } func Dial(rawURL string, server *srtp.Server) (*Client, error) { - u, err := url.Parse(rawURL) + conn, err := hap.Dial(rawURL) if err != nil { return nil, err } - query := u.Query() - conn := &hap.Client{ - DeviceAddress: u.Host, - DeviceID: query.Get("device_id"), - DevicePublic: hap.DecodeKey(query.Get("device_public")), - ClientID: query.Get("client_id"), - ClientPrivate: hap.DecodeKey(query.Get("client_private")), - } - - if err = conn.Dial(); err != nil { - return nil, err - } - client := &Client{ Connection: core.Connection{ ID: core.NewID(), diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go index ac2f14d7..2132266c 100644 --- a/pkg/homekit/proxy.go +++ b/pkg/homekit/proxy.go @@ -4,31 +4,30 @@ import ( "bufio" "bytes" "encoding/json" - "fmt" "io" "net" "net/http" + "time" "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/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) -func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc { +type ServerProxy interface { + ServerPair + AddConn(conn any) + DelConn(conn any) +} + +func ProxyHandler(srv ServerProxy, acc net.Conn) HandlerFunc { return func(con net.Conn) error { defer con.Close() - acc, err := dial() - if err != nil { - return err - } - defer acc.Close() - pr := &Proxy{ - con: con.(*secure.Conn), - acc: acc.(*secure.Conn), + con: con.(*hap.Conn), + acc: acc.(*hap.Conn), res: make(chan *http.Response), } @@ -36,17 +35,17 @@ func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFun go pr.handleAcc() // controller => accessory - return pr.handleCon(pair) + return pr.handleCon(srv) } } type Proxy struct { - con *secure.Conn - acc *secure.Conn + con *hap.Conn + acc *hap.Conn res chan *http.Response } -func (p *Proxy) handleCon(pair ServerPair) error { +func (p *Proxy) handleCon(srv ServerProxy) error { var hdsCharIID uint64 rd := bufio.NewReader(p.con) @@ -61,7 +60,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { switch { case req.Method == "POST" && req.URL.Path == hap.PathPairings: var res *http.Response - if res, err = handlePairings(p.con, req, pair); err != nil { + if res, err = handlePairings(req, srv); err != nil { return err } if err = res.Write(p.con); err != nil { @@ -117,7 +116,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { hdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort) // swtich accPort to conPort - hdsPort, err = p.listenHDS(hdsPort, hdsConSalt+hdsAccSalt) + hdsPort, err = p.listenHDS(srv, hdsPort, hdsConSalt+hdsAccSalt) if err != nil { return err } @@ -166,7 +165,8 @@ func (p *Proxy) handleAcc() error { } } -func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { +func (p *Proxy) listenHDS(srv ServerProxy, accPort int, salt string) (int, error) { + // The TCP port range for HDS must be >= 32768. ln, err := net.ListenTCP("tcp", nil) if err != nil { return 0, err @@ -175,30 +175,36 @@ func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { go func() { defer ln.Close() + _ = ln.SetDeadline(time.Now().Add(30 * time.Second)) + // raw controller conn - con, err := ln.Accept() + conn1, err := ln.Accept() if err != nil { return } - defer con.Close() + + defer conn1.Close() // secured controller conn (controlle=false because we are accessory) - con, err = hds.Client(con, p.con.SharedKey, salt, false) + con, err := hds.NewConn(conn1, p.con.SharedKey, salt, false) if err != nil { return } + srv.AddConn(con) + defer srv.DelConn(con) + accIP := p.acc.RemoteAddr().(*net.TCPAddr).IP // raw accessory conn - acc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", accIP, accPort)) + conn2, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: accIP, Port: accPort}) if err != nil { return } - defer acc.Close() + defer conn2.Close() // secured accessory conn (controller=true because we are controller) - acc, err = hds.Client(acc, p.acc.SharedKey, salt, true) + acc, err := hds.NewConn(conn2, p.acc.SharedKey, salt, true) if err != nil { return } diff --git a/pkg/homekit/server.go b/pkg/homekit/server.go index 2d00deab..75ba2a0f 100644 --- a/pkg/homekit/server.go +++ b/pkg/homekit/server.go @@ -15,15 +15,17 @@ import ( "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) +type HandlerFunc func(net.Conn) error + 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) + GetPair(id string) []byte + AddPair(id string, public []byte, permissions byte) + DelPair(id string) } type ServerAccessory interface { @@ -33,11 +35,11 @@ type ServerAccessory interface { GetImage(conn net.Conn, width, height int) []byte } -func ServerHandler(server Server) hap.HandlerFunc { +func ServerHandler(server Server) 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) + return handlePairings(req, server) case hap.PathAccessories: body := hap.JSONAccessories{Value: server.GetAccessories(conn)} @@ -103,7 +105,7 @@ func ServerHandler(server Server) hap.HandlerFunc { }) } -func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) hap.HandlerFunc { +func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) HandlerFunc { return func(conn net.Conn) error { rw := bufio.NewReaderSize(conn, 16*1024) wr := bufio.NewWriterSize(conn, 16*1024) @@ -130,7 +132,7 @@ func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response } } -func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Response, error) { +func handlePairings(req *http.Request, srv ServerPair) (*http.Response, error) { cmd := struct { Method byte `tlv8:"0"` Identifier string `tlv8:"1"` @@ -145,9 +147,9 @@ func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Re switch cmd.Method { case 3: // add - pair.AddPair(conn, cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) + srv.AddPair(cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) case 4: // delete - pair.DelPair(conn, cmd.Identifier) + srv.DelPair(cmd.Identifier) } body := struct { @@ -190,40 +192,3 @@ func makeResponse(mime string, v any) (*http.Response, error) { } 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/www/add.html b/www/add.html index 53d6b3dc..17c05059 100644 --- a/www/add.html +++ b/www/add.html @@ -100,7 +100,7 @@
- +
@@ -112,7 +112,7 @@