diff --git a/.gitignore b/.gitignore index 0d80b2fe..f0f61075 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ .tmp/ go2rtc.yaml + +go2rtc.json diff --git a/cmd/app/store/store.go b/cmd/app/store/store.go new file mode 100644 index 00000000..e18d1714 --- /dev/null +++ b/cmd/app/store/store.go @@ -0,0 +1,61 @@ +package store + +import ( + "encoding/json" + "github.com/rs/zerolog/log" + "os" +) + +const name = "go2rtc.json" + +var store map[string]interface{} + +func load() { + data, _ := os.ReadFile(name) + if data != nil { + if err := json.Unmarshal(data, &store); err != nil { + // TODO: log + log.Warn().Err(err).Msg("[app] read storage") + } + } + + if store == nil { + store = make(map[string]interface{}) + } +} + +func save() error { + data, err := json.Marshal(store) + if err != nil { + return err + } + + return os.WriteFile(name, data, 0644) +} + +func GetRaw(key string) interface{} { + if store == nil { + load() + } + + return store[key] +} + +func GetDict(key string) map[string]interface{} { + raw := GetRaw(key) + if raw != nil { + return raw.(map[string]interface{}) + } + + return make(map[string]interface{}) +} + +func Set(key string, v interface{}) error { + if store == nil { + load() + } + + store[key] = v + + return save() +} diff --git a/cmd/hass/hass.go b/cmd/hass/hass.go index c21d7e6d..e8f70605 100644 --- a/cmd/hass/hass.go +++ b/cmd/hass/hass.go @@ -63,16 +63,16 @@ func Init() { } urls[entrie.Title] = entrie.Options.StreamSource - //case "homekit_controller": - // if entrie.Data.ClientID == "" { - // continue - // } - // urls[entrie.Title] = fmt.Sprintf( - // "homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s", - // entrie.Data.DeviceHost, entrie.Data.DevicePort, - // entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic, - // entrie.Data.DeviceID, entrie.Data.DevicePublic, - // ) + case "homekit_controller": + if entrie.Data.ClientID == "" { + continue + } + urls[entrie.Title] = fmt.Sprintf( + "homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s", + entrie.Data.DeviceHost, entrie.Data.DevicePort, + entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic, + entrie.Data.DeviceID, entrie.Data.DevicePublic, + ) default: continue diff --git a/cmd/homekit/api.go b/cmd/homekit/api.go new file mode 100644 index 00000000..f21c7d3d --- /dev/null +++ b/cmd/homekit/api.go @@ -0,0 +1,157 @@ +package homekit + +import ( + "encoding/json" + "fmt" + "github.com/AlexxIT/go2rtc/cmd/app/store" + "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/homekit" + "github.com/AlexxIT/go2rtc/pkg/homekit/mdns" + "net/http" + "net/url" + "strings" +) + +func apiHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + items := make([]interface{}, 0) + + for name, src := range store.GetDict("streams") { + if src := src.(string); strings.HasPrefix(src, "homekit") { + u, err := url.Parse(src) + if err != nil { + continue + } + device := Device{ + Name: name, + Addr: u.Host, + Paired: true, + } + items = append(items, device) + } + } + + for info := range mdns.GetAll() { + if !strings.HasSuffix(info.Name, mdns.Suffix) { + continue + } + name := info.Name[:len(info.Name)-len(mdns.Suffix)] + device := Device{ + Name: strings.ReplaceAll(name, "\\", ""), + Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port), + } + for _, field := range info.InfoFields { + switch field[:2] { + case "id": + device.ID = field[3:] + case "md": + device.Model = field[3:] + case "sf": + device.Paired = field[3] == '0' + } + } + items = append(items, device) + } + + data, err := json.Marshal(items) + if err != nil { + log.Error().Err(err).Msg("[api.homekit]") + return + } + + if _, err = w.Write(data); err != nil { + log.Error().Err(err).Msg("[api.homekit]") + } + + case "POST": + // TODO: post params... + + id := r.URL.Query().Get("id") + pin := r.URL.Query().Get("pin") + + client, err := homekit.Pair(id, pin) + if err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] pair") + // response error + _, err = w.Write([]byte(err.Error())) + return + } + + name := r.URL.Query().Get("name") + dict := store.GetDict("streams") + dict[name] = client.URL() + if err = store.Set("streams", dict); err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] save to store") + // response error + _, err = w.Write([]byte(err.Error())) + } + + streams.New(name, client.URL()) + + case "DELETE": + src := r.URL.Query().Get("src") + dict := store.GetDict("streams") + for name, rawURL := range dict { + if name != src { + continue + } + + client, err := homekit.NewClient(rawURL.(string)) + if err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] new client") + // response error + _, err = w.Write([]byte(err.Error())) + return + } + + if err = client.Dial(); err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] client dial") + // response error + _, err = w.Write([]byte(err.Error())) + return + } + + go client.Handle() + + if err = client.ListPairings(); err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] unpair") + // response error + _, err = w.Write([]byte(err.Error())) + return + } + + if err = client.DeletePairing(client.ClientID); err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] unpair") + // response error + _, err = w.Write([]byte(err.Error())) + } + + delete(dict, name) + + if err = store.Set("streams", dict); err != nil { + // log error + log.Error().Err(err).Msg("[api.homekit] store set") + // response error + _, err = w.Write([]byte(err.Error())) + } + + return + } + } +} + +type Device struct { + ID string `json:"id"` + Name string `json:"name"` + Addr string `json:"addr"` + Model string `json:"model"` + Paired bool `json:"paired"` + //Type string `json:"type"` +} diff --git a/cmd/homekit/homekit.go b/cmd/homekit/homekit.go new file mode 100644 index 00000000..c39a2c13 --- /dev/null +++ b/cmd/homekit/homekit.go @@ -0,0 +1,39 @@ +package homekit + +import ( + "github.com/AlexxIT/go2rtc/cmd/api" + "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/cmd/streams" + "github.com/AlexxIT/go2rtc/pkg/homekit" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/rs/zerolog" +) + +func Init() { + log = app.GetLogger("homekit") + + streams.HandleFunc("homekit", streamHandler) + + api.HandleFunc("/api/homekit", apiHandler) +} + +var log zerolog.Logger + +func streamHandler(url string) (streamer.Producer, error) { + client, err := homekit.NewClient(url) + if err != nil { + return nil, err + } + if err = client.Dial(); err != nil { + return nil, err + } + + // start gorutine for reading responses from camera + go func() { + if err = client.Handle(); err != nil { + log.Warn().Err(err).Msg("[homekit] client") + } + }() + + return &Producer{client: client}, nil +} diff --git a/cmd/homekit/producer.go b/cmd/homekit/producer.go new file mode 100644 index 00000000..2b819ee8 --- /dev/null +++ b/cmd/homekit/producer.go @@ -0,0 +1,189 @@ +package homekit + +import ( + "errors" + "fmt" + "github.com/AlexxIT/go2rtc/cmd/srtp" + "github.com/AlexxIT/go2rtc/pkg/homekit" + "github.com/AlexxIT/go2rtc/pkg/homekit/camera" + pkg "github.com/AlexxIT/go2rtc/pkg/srtp" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/brutella/hap/characteristic" + "github.com/brutella/hap/rtp" + "net" + "strconv" +) + +type Producer struct { + streamer.Element + + client *homekit.Client + medias []*streamer.Media + tracks []*streamer.Track + + sessions []*pkg.Session +} + +func (c *Producer) GetMedias() []*streamer.Media { + if c.medias == nil { + c.medias = c.getMedias() + } + + return c.medias +} + +func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track { + for _, track := range c.tracks { + if track.Codec == codec { + return track + } + } + + track := &streamer.Track{Codec: codec, Direction: media.Direction} + c.tracks = append(c.tracks, track) + return track +} + +func (c *Producer) Start() error { + if c.tracks == nil { + return errors.New("producer without tracks") + } + + // get our server local IP-address + host, _, err := net.SplitHostPort(c.client.LocalAddr()) + if err != nil { + return err + } + + // get our server SRTP port + port, err := strconv.Atoi(srtp.Port) + if err != nil { + return err + } + + // setup HomeKit stream session + hkSession := camera.NewSession() + hkSession.SetLocalEndpoint(host, uint16(port)) + + // create client for processing camera accessory + cam := camera.NewClient(c.client) + // try to start HomeKit stream + if err = cam.StartStream2(hkSession); err != nil { + panic(err) // TODO: fixme + } + + // SRTP Video Session + vs := &pkg.Session{ + LocalSSRC: hkSession.Config.Video.RTP.Ssrc, + RemoteSSRC: hkSession.Answer.SsrcVideo, + Track: c.tracks[0], + } + if err = vs.SetKeys( + hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt, + hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt, + ); err != nil { + return err + } + + // SRTP Audio Session + as := &pkg.Session{ + LocalSSRC: hkSession.Config.Audio.RTP.Ssrc, + RemoteSSRC: hkSession.Answer.SsrcAudio, + Track: &streamer.Track{}, + } + if err = as.SetKeys( + hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt, + hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt, + ); err != nil { + return err + } + + srtp.AddSession(vs) + srtp.AddSession(as) + + c.sessions = []*pkg.Session{vs, as} + + return nil +} + +func (c *Producer) Stop() error { + err := c.client.Close() + + for _, session := range c.sessions { + srtp.RemoveSession(session) + } + + return err +} + +func (c *Producer) getMedias() []*streamer.Media { + var medias []*streamer.Media + + accs, err := c.client.GetAccessories() + acc := accs[0] + if err != nil { + panic(err) + } + + // get supported video config (not really necessary) + char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration) + v1 := &rtp.VideoStreamConfiguration{} + if err = char.ReadTLV8(v1); err != nil { + panic(err) + } + + for _, hkCodec := range v1.Codecs { + codec := &streamer.Codec{ClockRate: 90000} + + switch hkCodec.Type { + case rtp.VideoCodecType_H264: + codec.Name = streamer.CodecH264 + default: + panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type)) + } + + media := &streamer.Media{ + Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly, + Codecs: []*streamer.Codec{codec}, + } + medias = append(medias, media) + } + + char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration) + v2 := &rtp.AudioStreamConfiguration{} + if err = char.ReadTLV8(v2); err != nil { + panic(err) + } + + for _, hkCodec := range v2.Codecs { + codec := &streamer.Codec{ + Channels: uint16(hkCodec.Parameters.Channels), + } + + switch hkCodec.Type { + case rtp.AudioCodecType_AAC_ELD: + codec.Name = streamer.CodecAAC + default: + panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type)) + } + + switch hkCodec.Parameters.Samplerate { + case rtp.AudioCodecSampleRate8Khz: + codec.ClockRate = 8000 + case rtp.AudioCodecSampleRate16Khz: + codec.ClockRate = 16000 + case rtp.AudioCodecSampleRate24Khz: + codec.ClockRate = 24000 + default: + panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate)) + } + + media := &streamer.Media{ + Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly, + Codecs: []*streamer.Codec{codec}, + } + medias = append(medias, media) + } + + return medias +} diff --git a/cmd/srtp/srtp.go b/cmd/srtp/srtp.go new file mode 100644 index 00000000..8e19d4f4 --- /dev/null +++ b/cmd/srtp/srtp.go @@ -0,0 +1,59 @@ +package srtp + +import ( + "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/pkg/srtp" + "github.com/rs/zerolog" + "net" +) + +func Init() { + var cfg struct { + Mod struct { + Listen string `yaml:"listen"` + } `yaml:"srtp"` + } + + // default config + cfg.Mod.Listen = ":8443" + + // load config from YAML + app.LoadConfig(&cfg) + + if cfg.Mod.Listen == "" { + return + } + + log = app.GetLogger("srtp") + + // create SRTP server (endpoint) for receiving video from HomeKit camera + conn, err := net.ListenPacket("udp", cfg.Mod.Listen) + if err != nil { + log.Warn().Err(err).Msg("[srtp] listen") + } + + log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen") + + _, Port, _ = net.SplitHostPort(cfg.Mod.Listen) + + // run server + go func() { + server = &srtp.Server{} + if err = server.Serve(conn); err != nil { + log.Warn().Err(err).Msg("[srtp] serve") + } + }() +} + +var log zerolog.Logger +var server *srtp.Server + +var Port string + +func AddSession(session *srtp.Session) { + server.AddSession(session) +} + +func RemoveSession(session *srtp.Session) { + server.RemoveSession(session) +} \ No newline at end of file diff --git a/cmd/streams/streams.go b/cmd/streams/streams.go index b2829292..fb3d151e 100644 --- a/cmd/streams/streams.go +++ b/cmd/streams/streams.go @@ -2,6 +2,7 @@ package streams import ( "github.com/AlexxIT/go2rtc/cmd/app" + "github.com/AlexxIT/go2rtc/cmd/app/store" "github.com/rs/zerolog" ) @@ -17,6 +18,10 @@ func Init() { for name, item := range cfg.Mod { streams[name] = NewStream(item) } + + for name, item := range store.GetDict("streams") { + streams[name] = NewStream(item) + } } func Get(name string) *Stream { @@ -34,6 +39,10 @@ func Get(name string) *Stream { return nil } +func New(name string, source interface{}) { + streams[name] = NewStream(source) +} + func Delete(name string) { delete(streams, name) } diff --git a/go.mod b/go.mod index aa965dbd..01a516d1 100644 --- a/go.mod +++ b/go.mod @@ -3,39 +3,50 @@ module github.com/AlexxIT/go2rtc go 1.17 require ( + github.com/brutella/hap v0.0.17 github.com/deepch/vdk v0.0.19 github.com/gorilla/websocket v1.5.0 + github.com/hashicorp/mdns v1.0.5 github.com/pion/ice/v2 v2.2.6 github.com/pion/interceptor v0.1.11 github.com/pion/rtcp v1.2.9 github.com/pion/rtp v1.7.13 github.com/pion/sdp/v3 v3.0.5 + github.com/pion/srtp/v2 v2.0.10 github.com/pion/stun v0.3.5 github.com/pion/webrtc/v3 v3.1.43 github.com/rs/zerolog v1.27.0 github.com/stretchr/testify v1.7.1 + github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/brutella/dnssd v1.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-chi/chi v1.5.4 // indirect github.com/google/uuid v1.3.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect + github.com/miekg/dns v1.1.50 // indirect github.com/pion/datachannel v1.5.2 // indirect github.com/pion/dtls/v2 v2.1.5 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/mdns v0.0.5 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/sctp v1.8.2 // indirect - github.com/pion/srtp/v2 v2.0.10 // indirect github.com/pion/transport v0.13.1 // indirect github.com/pion/turn/v2 v2.0.8 // indirect github.com/pion/udp v0.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect + golang.org/x/mod v0.4.2 // indirect golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) replace ( diff --git a/go.sum b/go.sum index b790fc51..9ba33630 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ +github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78= +github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0= github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10 h1:4aKRthhmkYcStKuk1hcyvkeNJ/BDx5BTIvYmDO9ZJvg= github.com/AlexxIT/srtp/v2 v2.0.10-0.20220608200505-3191d4f19c10/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4= github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA= github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI= +github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0= +github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs= github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,6 +13,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -30,6 +36,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= +github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -42,6 +50,9 @@ github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZb github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -128,7 +139,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI= +github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE= +github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s= +github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -140,6 +156,8 @@ golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs= golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -152,7 +170,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -164,6 +186,8 @@ golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -176,7 +200,10 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -190,13 +217,17 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= diff --git a/main.go b/main.go index 0e49b2b0..860463f5 100644 --- a/main.go +++ b/main.go @@ -7,10 +7,12 @@ import ( "github.com/AlexxIT/go2rtc/cmd/exec" "github.com/AlexxIT/go2rtc/cmd/ffmpeg" "github.com/AlexxIT/go2rtc/cmd/hass" + "github.com/AlexxIT/go2rtc/cmd/homekit" "github.com/AlexxIT/go2rtc/cmd/mse" "github.com/AlexxIT/go2rtc/cmd/ngrok" "github.com/AlexxIT/go2rtc/cmd/rtmp" "github.com/AlexxIT/go2rtc/cmd/rtsp" + "github.com/AlexxIT/go2rtc/cmd/srtp" "github.com/AlexxIT/go2rtc/cmd/streams" "github.com/AlexxIT/go2rtc/cmd/webrtc" "os" @@ -33,6 +35,9 @@ func main() { webrtc.Init() mse.Init() + srtp.Init() + homekit.Init() + ngrok.Init() debug.Init() diff --git a/pkg/homekit/README.md b/pkg/homekit/README.md new file mode 100644 index 00000000..d0827519 --- /dev/null +++ b/pkg/homekit/README.md @@ -0,0 +1,33 @@ +# Homekit + +> PS. Character = Characteristic + +**Device** - HomeKit end device (swith, camera, etc) + +- mDNS name: `MyCamera._hap._tcp.local.` +- DeviceID - mac-like: `0E:AA:CE:2B:35:71` +- HomeKit device is described by: + - one or more `Accessories` - has `AID` and `Services` + - `Services` - has `IID`, `Type` and `Characters` + - `Characters` - has `IID`, `Type`, `Format` and `Value` + +**Client** - HomeKit client (iPhone, iPad, MacBook or opensource library) + +- ClientID - static random UUID +- ClientPublic/ClientPrivate - static random 32 byte keypair +- can pair with Device (exchange ClientID/ClientPublic, ServerID/ServerPublic using Pin) +- can auth to Device using ClientPrivate +- holding persistant Secure connection to device +- can read device Accessories +- can read and write device Characters +- can subscribe on device Characters change (Event) + +**Server** - HomeKit server (soft on end device or opensource library) + +- ServerID - same as DeviceID (using for Client auth) +- ServerPublic/ServerPrivate - static random 32 byte keypair + +## Useful links + +- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys) +- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py) diff --git a/pkg/homekit/accessory.go b/pkg/homekit/accessory.go new file mode 100644 index 00000000..1b14fc72 --- /dev/null +++ b/pkg/homekit/accessory.go @@ -0,0 +1,62 @@ +package homekit + +type Accessory struct { + AID int `json:"aid"` + Services []*Service `json:"services"` +} + +type Accessories struct { + Accessories []*Accessory `json:"accessories"` +} + +type Characters struct { + Characters []*Character `json:"characteristics"` +} + +func (a *Accessory) GetService(servType string) *Service { + for _, serv := range a.Services { + if serv.Type == servType { + return serv + } + } + return nil +} + +func (a *Accessory) GetCharacter(charType string) *Character { + for _, serv := range a.Services { + for _, char := range serv.Characters { + if char.Type == charType { + return char + } + } + } + return nil +} + +func (a *Accessory) GetCharacterByID(iid int) *Character { + for _, serv := range a.Services { + for _, char := range serv.Characters { + if char.IID == iid { + return char + } + } + } + return nil +} + +type Service struct { + IID int `json:"iid"` + Type string `json:"type"` + Primary bool `json:"primary,omitempty"` + Hidden bool `json:"hidden,omitempty"` + Characters []*Character `json:"characteristics"` +} + +func (s *Service) GetCharacter(charType string) *Character { + for _, char := range s.Characters { + if char.Type == charType { + return char + } + } + return nil +} diff --git a/pkg/homekit/camera/client.go b/pkg/homekit/camera/client.go new file mode 100644 index 00000000..22a7fa7d --- /dev/null +++ b/pkg/homekit/camera/client.go @@ -0,0 +1,100 @@ +package camera + +import ( + "errors" + "github.com/AlexxIT/go2rtc/pkg/homekit" + "github.com/brutella/hap/characteristic" + "github.com/brutella/hap/rtp" +) + +type Client struct { + client *homekit.Client +} + +func NewClient(client *homekit.Client) *Client { + return &Client{client: client} +} + +func (c *Client) StartStream2(ses *Session) (err error) { + // Step 1. Check if camera ready (free) to stream + var srv *homekit.Service + if srv, err = c.GetFreeStream(); err != nil { + return err + } + if srv == nil { + return errors.New("no free streams") + } + + if ses.Answer, err = c.SetupEndpoins(srv, ses.Offer); err != nil { + return + } + + return c.SetConfig(srv, ses.Config) +} + +// GetFreeStream search free streaming service. +// Usual every HomeKit camera can stream only to two clients simultaniosly. +// So it has two similar services for streaming. +func (c *Client) GetFreeStream() (srv *homekit.Service, err error) { + var accs []*homekit.Accessory + if accs, err = c.client.GetAccessories(); err != nil { + return + } + + for _, srv = range accs[0].Services { + for _, char := range srv.Characters { + if char.Type == characteristic.TypeStreamingStatus { + status := rtp.StreamingStatus{} + if err = char.ReadTLV8(&status); err != nil { + return + } + + if status.Status == rtp.SessionStatusSuccess { + return + } + } + } + } + + return nil, nil +} + +func (c *Client) SetupEndpoins( + srv *homekit.Service, req *rtp.SetupEndpoints, +) (res *rtp.SetupEndpointsResponse, err error) { + // get setup endpoint character ID + char := srv.GetCharacter(characteristic.TypeSetupEndpoints) + char.Event = nil + // encode new character value + if err = char.Write(req); err != nil { + return + } + // write (put) new endpoint value to device + if err = c.client.PutCharacters(char); err != nil { + return + } + + // get new endpoint value from device (response) + if err = c.client.GetCharacter(char); err != nil { + return + } + // decode new endpoint value + res = &rtp.SetupEndpointsResponse{} + if err = char.ReadTLV8(res); err != nil { + return + } + + return +} + +func (c *Client) SetConfig(srv *homekit.Service, config *rtp.StreamConfiguration) (err error) { + // get setup endpoint character ID + char := srv.GetCharacter(characteristic.TypeSelectedStreamConfiguration) + char.Event = nil + // encode new character value + if err = char.Write(config); err != nil { + panic(err) + } + // write (put) new character value to device + return c.client.PutCharacters(char) +} diff --git a/pkg/homekit/camera/session.go b/pkg/homekit/camera/session.go new file mode 100644 index 00000000..0647f5ad --- /dev/null +++ b/pkg/homekit/camera/session.go @@ -0,0 +1,103 @@ +package camera + +import ( + cryptorand "crypto/rand" + "encoding/binary" + "github.com/brutella/hap/rtp" +) + +type Session struct { + Offer *rtp.SetupEndpoints + Answer *rtp.SetupEndpointsResponse + Config *rtp.StreamConfiguration +} + +func NewSession() *Session { + sessionID := RandomBytes(16) + s := &Session{ + Offer: &rtp.SetupEndpoints{ + SessionId: sessionID, + Video: rtp.CryptoSuite{ + MasterKey: RandomBytes(16), + MasterSalt: RandomBytes(14), + }, + Audio: rtp.CryptoSuite{ + MasterKey: RandomBytes(16), + MasterSalt: RandomBytes(14), + }, + }, + Config: &rtp.StreamConfiguration{ + Command: rtp.SessionControlCommand{ + Identifier: sessionID, + Type: rtp.SessionControlCommandTypeStart, + }, + Video: rtp.VideoParameters{ + CodecType: rtp.VideoCodecType_H264, + CodecParams: rtp.VideoCodecParameters{ + Profiles: []rtp.VideoCodecProfile{ + {Id: rtp.VideoCodecProfileMain}, + }, + Levels: []rtp.VideoCodecLevel{ + {Level: rtp.VideoCodecLevel4}, + }, + Packetizations: []rtp.VideoCodecPacketization{ + {Mode: rtp.VideoCodecPacketizationModeNonInterleaved}, + }, + }, + Attributes: rtp.VideoCodecAttributes{ + Width: 1920, Height: 1080, Framerate: 30, + }, + RTP: rtp.RTPParams{ + PayloadType: 99, + Ssrc: RandomUint32(), + Bitrate: 299, + Interval: 0.5, + ComfortNoisePayloadType: 98, + MTU: 0, + }, + }, + Audio: rtp.AudioParameters{ + CodecType: rtp.AudioCodecType_AAC_ELD, + CodecParams: rtp.AudioCodecParameters{ + Channels: 1, + Bitrate: rtp.AudioCodecBitrateVariable, + Samplerate: rtp.AudioCodecSampleRate16Khz, + PacketTime: 30, + }, + RTP: rtp.RTPParams{ + PayloadType: 110, + Ssrc: RandomUint32(), + Bitrate: 24, + Interval: 5, + MTU: 13, + }, + ComfortNoise: false, + }, + }, + } + return s +} + +func (s *Session) SetLocalEndpoint(host string, port uint16) { + s.Offer.ControllerAddr = rtp.Addr{ + IPAddr: host, + VideoRtpPort: port, + AudioRtpPort: port, + } +} + +func (s *Session) SetVideo() { + +} + +func RandomBytes(size int) []byte { + data := make([]byte, size) + _, _ = cryptorand.Read(data) + return data +} + +func RandomUint32() uint32 { + data := make([]byte, 4) + _, _ = cryptorand.Read(data) + return binary.BigEndian.Uint32(data) +} diff --git a/pkg/homekit/character.go b/pkg/homekit/character.go new file mode 100644 index 00000000..0be4e14a --- /dev/null +++ b/pkg/homekit/character.go @@ -0,0 +1,133 @@ +package homekit + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "github.com/brutella/hap/characteristic" + "github.com/brutella/hap/tlv8" + "io" + "net/http" +) + +type Character struct { + AID int `json:"aid,omitempty"` + IID int `json:"iid"` + Type string `json:"type,omitempty"` + Format string `json:"format,omitempty"` + Value interface{} `json:"value,omitempty"` + Event interface{} `json:"ev,omitempty"` + Perms []string `json:"perms,omitempty"` + Description string `json:"description,omitempty"` + //MaxDataLen int `json:"maxDataLen"` + + listeners map[io.Writer]bool +} + +func (c *Character) AddListener(w io.Writer) { + // TODO: sync.Mutex + if c.listeners == nil { + c.listeners = map[io.Writer]bool{} + } + c.listeners[w] = true +} + +func (c *Character) RemoveListener(w io.Writer) { + delete(c.listeners, w) + + if len(c.listeners) == 0 { + c.listeners = nil + } +} + +func (c *Character) NotifyListeners(ignore io.Writer) error { + if c.listeners == nil { + return nil + } + + data, err := c.GenerateEvent() + if err != nil { + return err + } + + for w, _ := range c.listeners { + if w == ignore { + continue + } + if _, err = w.Write(data); err != nil { + // error not a problem - just remove listener + c.RemoveListener(w) + } + } + + return nil +} + +// GenerateEvent with raw HTTP headers +func (c *Character) GenerateEvent() (data []byte, err error) { + chars := Characters{ + Characters: []*Character{{AID: DeviceAID, IID: c.IID, Value: c.Value}}, + } + if data, err = json.Marshal(chars); err != nil { + return + } + + res := http.Response{ + StatusCode: http.StatusOK, + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header{"Content-Type": []string{MimeJSON}}, + ContentLength: int64(len(data)), + Body: io.NopCloser(bytes.NewReader(data)), + } + + buf := bytes.NewBuffer([]byte{0}) + if err = res.Write(buf); err != nil { + return + } + copy(buf.Bytes(), "EVENT") + + return buf.Bytes(), err +} + +// Set new value and NotifyListeners +func (c *Character) Set(v interface{}) (err error) { + if err = c.Write(v); err != nil { + return + } + return c.NotifyListeners(nil) +} + +// Write new value with right format +func (c *Character) Write(v interface{}) (err error) { + switch c.Format { + case characteristic.FormatTLV8: + var data []byte + if data, err = tlv8.Marshal(v); err != nil { + return + } + c.Value = base64.StdEncoding.EncodeToString(data) + + case characteristic.FormatBool: + switch v.(type) { + case bool: + c.Value = v.(bool) + case float64: + c.Value = v.(float64) != 0 + } + } + return +} + +// ReadTLV8 value to right struct +func (c *Character) ReadTLV8(v interface{}) (err error) { + var data []byte + if data, err = base64.StdEncoding.DecodeString(c.Value.(string)); err != nil { + return + } + return tlv8.Unmarshal(data, v) +} + +func (c *Character) ReadBool() bool { + return c.Value.(bool) +} diff --git a/pkg/homekit/client.go b/pkg/homekit/client.go new file mode 100644 index 00000000..302b8423 --- /dev/null +++ b/pkg/homekit/client.go @@ -0,0 +1,732 @@ +package homekit + +import ( + "bufio" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/AlexxIT/go2rtc/pkg/homekit/mdns" + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/brutella/hap" + "github.com/brutella/hap/chacha20poly1305" + "github.com/brutella/hap/curve25519" + "github.com/brutella/hap/ed25519" + "github.com/brutella/hap/hkdf" + "github.com/brutella/hap/tlv8" + "github.com/tadglines/go-pkgs/crypto/srp" + "io" + "net" + "net/http" + "net/url" + "strings" +) + +// Client for HomeKit. DevicePublic can be null. +type Client struct { + streamer.Element + + DeviceAddress string // including port + DeviceID string + DevicePublic []byte + ClientID string + ClientPrivate []byte + + OnEvent func(res *http.Response) + Output func(msg interface{}) + + conn net.Conn + secure *Secure + httpResponse chan *bufio.Reader +} + +func NewClient(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + + query := u.Query() + c := &Client{ + DeviceAddress: u.Host, + DeviceID: query.Get("device_id"), + DevicePublic: DecodeKey(query.Get("device_public")), + ClientID: query.Get("client_id"), + ClientPrivate: DecodeKey(query.Get("client_private")), + } + + return c, nil +} + +func Pair(deviceID, pin string) (*Client, error) { + entry := mdns.GetEntry(deviceID) + if entry == nil { + return nil, errors.New("can't find device via mDNS") + } + + c := &Client{ + DeviceAddress: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port), + DeviceID: deviceID, + ClientID: GenerateUUID(), + ClientPrivate: GenerateKey(), + } + + var mfi bool + for _, field := range entry.InfoFields { + if field[:2] == "ff" { + if field[3] == '1' { + mfi = true + } + break + } + } + + return c, c.Pair(mfi, pin) +} + +func (c *Client) ClientPublic() []byte { + return c.ClientPrivate[32:] +} + +func (c *Client) URL() string { + return fmt.Sprintf( + "homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x", + c.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate, + ) +} + +func (c *Client) DialAndServe() error { + if err := c.Dial(); err != nil { + return err + } + return c.Handle() +} + +func (c *Client) Dial() error { + // update device host before dial + if host := mdns.GetAddress(c.DeviceID); host != "" { + c.DeviceAddress = host + } + + var err error + c.conn, err = net.Dial("tcp", c.DeviceAddress) + if err != nil { + return err + } + + // STEP M1: send our session public to device + sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() + + // 1. generate payload + // important not include other fields + requestM1 := struct { + State byte `tlv8:"6"` + PublicKey []byte `tlv8:"3"` + }{ + State: hap.M1, + PublicKey: sessionPublic[:], + } + // 2. pack payload to TLV8 + buf, err := tlv8.Marshal(requestM1) + if err != nil { + return err + } + + // 3. send request + resp, err := c.Post(UriPairVerify, buf) + if err != nil { + return err + } + + // STEP M2: unpack deviceID from response + responseM2 := PairVerifyPayload{} + if err = tlv8.UnmarshalReader(resp.Body, &responseM2); err != nil { + return err + } + + // 1. generate session shared key + var deviceSessionPublic [32]byte + copy(deviceSessionPublic[:], responseM2.PublicKey) + sessionShared := curve25519.SharedSecret(sessionPrivate, deviceSessionPublic) + sessionKey, err := hkdf.Sha512( + sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"), + []byte("Pair-Verify-Encrypt-Info"), + ) + + // 2. decrypt M2 response with session key + msg := responseM2.EncryptedData[:len(responseM2.EncryptedData)-16] + var mac [16]byte + copy(mac[:], responseM2.EncryptedData[len(msg):]) // 16 byte (MAC) + + buf, err = chacha20poly1305.DecryptAndVerify( + sessionKey[:], []byte("PV-Msg02"), msg, mac, nil, + ) + + // 3. unpack payload from TLV8 + payloadM2 := PairVerifyPayload{} + if err = tlv8.Unmarshal(buf, &payloadM2); err != nil { + return err + } + + // 4. verify signature for M2 response with device public + // device session + device id + our session + if c.DevicePublic != nil { + buf = nil + buf = append(buf, responseM2.PublicKey[:]...) + buf = append(buf, []byte(payloadM2.Identifier)...) + buf = append(buf, sessionPublic[:]...) + if !ed25519.ValidateSignature( + c.DevicePublic[:], buf, payloadM2.Signature, + ) { + return errors.New("device public signature invalid") + } + } + + // STEP M3: send our clientID to device + // 1. generate signature with our private key + // (our session + our ID + device session) + buf = nil + buf = append(buf, sessionPublic[:]...) + buf = append(buf, []byte(c.ClientID)...) + buf = append(buf, responseM2.PublicKey[:]...) + signature, err := ed25519.Signature(c.ClientPrivate[:], buf) + if err != nil { + return err + } + + // 2. generate payload + payloadM3 := struct { + Identifier string `tlv8:"1"` + Signature []byte `tlv8:"10"` + }{ + Identifier: c.ClientID, + Signature: signature, + } + // 3. pack payload to TLV8 + buf, err = tlv8.Marshal(payloadM3) + if err != nil { + return err + } + + // 4. encrypt payload with session key + msg, mac, _ = chacha20poly1305.EncryptAndSeal( + sessionKey[:], []byte("PV-Msg03"), buf, nil, + ) + + // 4. generate request + requestM3 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + }{ + State: hap.M3, + EncryptedData: append(msg, mac[:]...), + } + // 5. pack payload to TLV8 + buf, err = tlv8.Marshal(requestM3) + if err != nil { + return err + } + + resp, err = c.Post(UriPairVerify, buf) + if err != nil { + return err + } + + // STEP M4. Read response + responseM4 := PairVerifyPayload{} + if err = tlv8.UnmarshalReader(resp.Body, &responseM4); err != nil { + return err + } + + // 1. check response state + if responseM4.State != 4 || responseM4.Status != 0 { + return fmt.Errorf("wrong M4 response: %+v", responseM4) + } + + c.secure, err = NewSecure(sessionShared, false) + //c.secure.Buffer = bytes.NewBuffer(nil) + c.secure.Conn = c.conn + + c.httpResponse = make(chan *bufio.Reader, 10) + + return err +} + +// https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c +func (c *Client) Pair(mfi bool, pin string) (err error) { + pin = strings.ReplaceAll(pin, "-", "") + if len(pin) != 8 { + return fmt.Errorf("wrong PIN format: %s", pin) + } + pin = pin[:3] + "-" + pin[3:5] + "-" + pin[5:] + + c.conn, err = net.Dial("tcp", c.DeviceAddress) + if err != nil { + return + } + + // STEP M1. Generate request + reqM1 := struct { + Method byte `tlv8:"0"` + State byte `tlv8:"6"` + }{ + State: hap.M1, + } + if mfi { + reqM1.Method = 1 // ff=1 => method=1, ff=2 => method=0 + } + buf, err := tlv8.Marshal(reqM1) + if err != nil { + return + } + + // STEP M1. Send request + res, err := c.Post(UriPairSetup, buf) + if err != nil { + return + } + + // STEP M2. Read response + resM2 := struct { + Salt []byte `tlv8:"2"` + PublicKey []byte `tlv8:"3"` // server public key, aka session.B + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + }{} + if err = tlv8.UnmarshalReader(res.Body, &resM2); err != nil { + return + } + if resM2.State != 2 || resM2.Error > 0 { + return fmt.Errorf("wrong M2: %+v", resM2) + } + + // STEP M3. Generate session using pin + username := []byte("Pair-Setup") + + SRP, err := srp.NewSRP( + "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), + ) + if err != nil { + return + } + + SRP.SaltLength = 16 + + // username: "Pair-Setup" + // password: PIN (with dashes) + session := SRP.NewClientSession(username, []byte(pin)) + sessionShared, err := session.ComputeKey(resM2.Salt, resM2.PublicKey) + if err != nil { + return + } + + // STEP M3. Generate request + reqM3 := struct { + PublicKey []byte `tlv8:"3"` + Proof []byte `tlv8:"4"` + State byte `tlv8:"6"` + }{ + PublicKey: session.GetA(), // client public key, aka session.A + Proof: session.ComputeAuthenticator(), + State: hap.M3, + } + buf, err = tlv8.Marshal(reqM3) + if err != nil { + return err + } + + // STEP M3. Send request + res, err = c.Post(UriPairSetup, buf) + if err != nil { + return + } + + // STEP M4. Read response + resM4 := struct { + Proof []byte `tlv8:"4"` // server proof + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + }{} + if err = tlv8.UnmarshalReader(res.Body, &resM4); err != nil { + return + } + if resM4.Error == 2 { + return fmt.Errorf("wrong PIN: %s", pin) + } + if resM4.State != 4 || resM4.Error > 0 { + return fmt.Errorf("wrong M4: %+v", resM4) + } + + // STEP M4. Verify response + if !session.VerifyServerAuthenticator(resM4.Proof) { + return errors.New("verify server auth fail") + } + + // STEP M5. Generate signature + saltKey, err := hkdf.Sha512( + sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"), + []byte("Pair-Setup-Controller-Sign-Info"), + ) + if err != nil { + return + } + + buf = nil + buf = append(buf, saltKey[:]...) + buf = append(buf, []byte(c.ClientID)...) + buf = append(buf, c.ClientPublic()...) + + signature, err := ed25519.Signature(c.ClientPrivate, buf) + if err != nil { + return + } + + // STEP M5. Generate payload + msgM5 := struct { + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Signature []byte `tlv8:"10"` + }{ + Identifier: c.ClientID, + PublicKey: c.ClientPublic(), + Signature: signature, + } + buf, err = tlv8.Marshal(msgM5) + if err != nil { + return + } + + // STEP M5. Encrypt payload + sessionKey, err := hkdf.Sha512( + sessionShared, []byte("Pair-Setup-Encrypt-Salt"), + []byte("Pair-Setup-Encrypt-Info"), + ) + if err != nil { + return + } + buf, mac, _ := chacha20poly1305.EncryptAndSeal( + sessionKey[:], []byte("PS-Msg05"), buf, nil, + ) + + // STEP M5. Generate request + reqM5 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + }{ + EncryptedData: append(buf, mac[:]...), + State: hap.M5, + } + buf, err = tlv8.Marshal(reqM5) + if err != nil { + return err + } + + // STEP M5. Send request + res, err = c.Post(UriPairSetup, buf) + if err != nil { + return + } + + // STEP M6. Read response + resM6 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + }{} + if err = tlv8.UnmarshalReader(res.Body, &resM6); err != nil { + return + } + if resM6.State != 6 || resM6.Error > 0 { + return fmt.Errorf("wrong M6: %+v", resM2) + } + + // STEP M6. Decrypt payload + msg := resM6.EncryptedData[:len(resM6.EncryptedData)-16] + copy(mac[:], resM6.EncryptedData[len(msg):]) // 16 byte (MAC) + + buf, err = chacha20poly1305.DecryptAndVerify( + sessionKey[:], []byte("PS-Msg06"), msg, mac, nil, + ) + if err != nil { + return + } + + msgM6 := struct { + Identifier []byte `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Signature []byte `tlv8:"10"` + }{} + if err = tlv8.Unmarshal(buf, &msgM6); err != nil { + return + } + + // STEP M6. Verify payload + if saltKey, err = hkdf.Sha512( + sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"), + []byte("Pair-Setup-Accessory-Sign-Info"), + ); err != nil { + return + } + + buf = nil + buf = append(buf, saltKey[:]...) + buf = append(buf, msgM6.Identifier...) + buf = append(buf, msgM6.PublicKey...) + + if !ed25519.ValidateSignature( + msgM6.PublicKey[:], buf, msgM6.Signature, + ) { + return errors.New("wrong server signature") + } + + if c.DeviceID != string(msgM6.Identifier) { + return fmt.Errorf("wrong Device ID: %s", msgM6.Identifier) + } + + c.DevicePublic = msgM6.PublicKey + + return nil +} + +func (c *Client) Close() error { + if c.conn == nil { + return nil + } + conn := c.conn + c.conn = nil + return conn.Close() +} + +func (c *Client) GetAccessories() ([]*Accessory, error) { + res, err := c.Get("/accessories") + if err != nil { + return nil, err + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + p := Accessories{} + if err = json.Unmarshal(data, &p); err != nil { + return nil, err + } + + for _, accs := range p.Accessories { + for _, serv := range accs.Services { + for _, char := range serv.Characters { + char.AID = accs.AID + } + } + } + + return p.Accessories, nil +} + +func (c *Client) GetCharacters(query string) ([]*Character, error) { + res, err := c.Get("/characteristics?id=" + query) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + ch := Characters{} + if err = json.Unmarshal(data, &ch); err != nil { + return nil, err + } + return ch.Characters, nil +} + +func (c *Client) GetCharacter(char *Character) error { + query := fmt.Sprintf("%d.%d", char.AID, char.IID) + chars, err := c.GetCharacters(query) + if err != nil { + return err + } + char.Value = chars[0].Value + return nil +} + +func (c *Client) PutCharacters(characters ...*Character) (err error) { + for i, char := range characters { + if char.Event != nil { + char = &Character{AID: char.AID, IID: char.IID, Event: char.Event} + } else { + char = &Character{AID: char.AID, IID: char.IID, Value: char.Value} + } + characters[i] = char + } + var data []byte + if data, err = json.Marshal(Characters{characters}); err != nil { + return + } + + var res *http.Response + if res, err = c.Put("/characteristics", data); err != nil { + return + } + + if res.StatusCode >= 400 { + return errors.New("wrong response status") + } + + return +} + +func (c *Client) GetImage(width, height int) ([]byte, error) { + res, err := c.Post( + "/resource", []byte(fmt.Sprintf( + `{"image-width":%d,"image-height":%d,"resource-type":"image","reason":0}`, + width, height, + )), + ) + if err != nil { + return nil, err + } + return io.ReadAll(res.Body) +} + +//func (c *Client) onEventData(r io.Reader) error { +// if c.OnEvent == nil { +// return nil +// } +// +// data, err := io.ReadAll(r) +// +// ch := Characters{} +// if err = json.Unmarshal(data, &ch); err != nil { +// return err +// } +// +// c.OnEvent(ch.Characters) +// +// return nil +//} + +func (c *Client) ListPairings() error { + pReq := struct { + Method byte `tlv8:"0"` + State byte `tlv8:"6"` + }{ + Method: hap.MethodListPairings, + State: hap.M1, + } + data, err := tlv8.Marshal(pReq) + if err != nil { + return err + } + + res, err := c.Post("/pairings", data) + if err != nil { + return err + } + + data, err = io.ReadAll(res.Body) + // TODO: don't know how to fix array of items + var pRes struct { + State byte `tlv8:"6"` + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Permission byte `tlv8:"11"` + } + if err = tlv8.Unmarshal(data, &pRes); err != nil { + return err + } + + return nil +} + +func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error { + pReq := struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + State byte `tlv8:"6"` + Permission byte `tlv8:"11"` + }{ + Method: hap.MethodAddPairing, + Identifier: clientID, + PublicKey: clientPublic, + State: hap.M1, + Permission: hap.PermissionUser, + } + if admin { + pReq.Permission = hap.PermissionAdmin + } + + data, err := tlv8.Marshal(pReq) + if err != nil { + return err + } + + res, err := c.Post("/pairings", data) + if err != nil { + return err + } + + data, err = io.ReadAll(res.Body) + var pRes struct { + State byte `tlv8:"6"` + Unknown byte `tlv8:"7"` + } + if err = tlv8.Unmarshal(data, &pRes); err != nil { + return err + } + + return nil +} + +func (c *Client) DeletePairing(id string) error { + reqM1 := struct { + State byte `tlv8:"6"` + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + }{ + State: hap.M1, + Method: hap.MethodDeletePairing, + Identifier: id, + } + data, err := tlv8.Marshal(reqM1) + if err != nil { + return err + } + + res, err := c.Post("/pairings", data) + if err != nil { + return err + } + + data, err = io.ReadAll(res.Body) + var resM2 struct { + State byte `tlv8:"6"` + } + if err = tlv8.Unmarshal(data, &resM2); err != nil { + return err + } + if resM2.State != hap.M2 { + return errors.New("wrong state") + } + + return nil +} + +func (c *Client) LocalAddr() string { + return c.conn.LocalAddr().String() +} + +func DecodeKey(s string) []byte { + if s == "" { + return nil + } + data, err := hex.DecodeString(s) + if err != nil { + return nil + } + return data +} diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go new file mode 100644 index 00000000..3ee616f6 --- /dev/null +++ b/pkg/homekit/helpers.go @@ -0,0 +1,90 @@ +package homekit + +import ( + "crypto/rand" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" +) + +const DeviceAID = 1 // TODO: fix someday + +func GenerateID(name string) string { + sum := sha512.Sum512([]byte(name)) + return fmt.Sprintf( + "%02X:%02X:%02X:%02X:%02X:%02X", + sum[0], sum[1], sum[2], sum[3], sum[4], sum[5], + ) +} + +func GenerateUUID() string { + //12345678-9012-3456-7890-123456789012 + data := make([]byte, 16) + _, _ = rand.Read(data) + s := hex.EncodeToString(data) + return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] +} + +type PairVerifyPayload struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + Status byte `tlv8:"7"` + Signature []byte `tlv8:"10"` +} + +//func (c *Character) Unmarshal(value interface{}) error { +// switch c.Format { +// case characteristic.FormatTLV8: +// data, err := base64.StdEncoding.DecodeString(c.Value.(string)) +// if err != nil { +// return err +// } +// return tlv8.Unmarshal(data, value) +// } +// return nil +//} + +//func (c *Character) Marshal(value interface{}) error { +// switch c.Format { +// case characteristic.FormatTLV8: +// data, err := tlv8.Marshal(value) +// if err != nil { +// return err +// } +// c.Value = base64.StdEncoding.EncodeToString(data) +// } +// return nil +//} + +func (c *Character) String() string { + data, err := json.Marshal(c) + if err != nil { + return "ERROR" + } + return string(data) +} + +func UnmarshalEvent(res *http.Response) (char *Character, err error) { + var data []byte + if data, err = io.ReadAll(res.Body); err != nil { + return + } + + ch := Characters{} + if err = json.Unmarshal(data, &ch); err != nil { + return + } + + if len(ch.Characters) > 1 { + panic("not implemented") + } + + char = ch.Characters[0] + return +} diff --git a/pkg/homekit/http.go b/pkg/homekit/http.go new file mode 100644 index 00000000..de0c21ff --- /dev/null +++ b/pkg/homekit/http.go @@ -0,0 +1,246 @@ +package homekit + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net/http" + "net/textproto" + "strconv" +) + +const ( + MimeTLV8 = "application/pairing+tlv8" + MimeJSON = "application/hap+json" + + UriPairSetup = "/pair-setup" + UriPairVerify = "/pair-verify" + UriPairings = "/pairings" + UriAccessories = "/accessories" + UriCharacteristics = "/characteristics" + UriResource = "/resource" +) + +func (c *Client) Write(p []byte) (r io.Reader, err error) { + if c.secure == nil { + if _, err = c.conn.Write(p); err == nil { + r = bufio.NewReader(c.conn) + } + } else { + if _, err = c.secure.Write(p); err == nil { + r = <-c.httpResponse + } + } + return +} + +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.secure == nil { + // insecure requests + if err := req.Write(c.conn); err != nil { + return nil, err + } + return http.ReadResponse(bufio.NewReader(c.conn), req) + } + + // secure support write interface to connection + if err := req.Write(c.secure); err != nil { + return nil, err + } + + // get decrypted buffer from connection + buf := <-c.httpResponse + + return http.ReadResponse(buf, req) +} + +func (c *Client) Get(uri string) (*http.Response, error) { + req, err := http.NewRequest( + "GET", "http://"+c.DeviceAddress+uri, nil, + ) + if err != nil { + return nil, err + } + return c.Do(req) +} + +func (c *Client) Post(uri string, data []byte) (*http.Response, error) { + req, err := http.NewRequest( + "POST", "http://"+c.DeviceAddress+uri, + bytes.NewReader(data), + ) + if err != nil { + return nil, err + } + + switch uri { + case "/pair-verify", "/pairings": + req.Header.Set("Content-Type", MimeTLV8) + case UriResource: + req.Header.Set("Content-Type", MimeJSON) + } + + return c.Do(req) +} + +func (c *Client) Put(uri string, data []byte) (*http.Response, error) { + req, err := http.NewRequest( + "PUT", "http://"+c.DeviceAddress+uri, + bytes.NewReader(data), + ) + if err != nil { + return nil, err + } + + switch uri { + case UriCharacteristics: + req.Header.Set("Content-Type", MimeJSON) + } + + return c.Do(req) +} + +func (c *Client) Handle() (err error) { + defer func() { + if c.conn == nil { + err = nil + } + }() + + b := make([]byte, 512000) + for { + var total, content int + header := -1 + + for { + var n1 int + n1, err = c.secure.Read(b[total:]) + if err != nil { + return err + } + + if n1 == 0 { + return io.EOF + } + + total += n1 + + // TODO: rewrite + if header == -1 { + // step 1. wait whole header + header = bytes.Index(b[:total], []byte("\r\n\r\n")) + if header < 0 { + continue + } + header += 4 + + // step 2. check content-length + i1 := bytes.Index(b[:total], []byte("Content-Length: ")) + if i1 < 0 { + break + } + i1 += 16 + i2 := bytes.IndexByte(b[i1:total], '\r') + content, err = strconv.Atoi(string(b[i1 : i1+i2])) + if err != nil { + break + } + } + + if total >= header+content { + break + } + } + + // copy slice to buffer + buf := bytes.NewBuffer(make([]byte, 0, total)) + buf.Write(b[:total]) + r := bufio.NewReader(buf) + + // EVENT/1.0 200 OK + if b[0] == 'E' { + if c.OnEvent == nil { + continue + } + + tp := textproto.NewReader(r) + + var s string + if s, err = tp.ReadLine(); err != nil { + return err + } + if s != "EVENT/1.0 200 OK" { + return errors.New("wrong response") + } + + var mimeHeader textproto.MIMEHeader + if mimeHeader, err = tp.ReadMIMEHeader(); err != nil { + return err + } + + var cl int + if cl, err = strconv.Atoi( + mimeHeader.Get("Content-Length"), + ); err != nil { + return err + } + + res := http.Response{ + StatusCode: 200, + Proto: "EVENT/1.0", + ProtoMajor: 1, + ProtoMinor: 0, + Header: http.Header(mimeHeader), + ContentLength: int64(cl), + Body: io.NopCloser(r), + } + c.OnEvent(&res) + continue + } + + //if bytes.Index(b, []byte("image/jpeg")) > 0 { + // if total, err = c.secure.Read(b); err != nil { + // return + // } + // buf.Write(b[:total]) + //} + + c.httpResponse <- r + } +} + +func WriteStatusCode(w io.Writer, statusCode int) (err error) { + body := []byte(fmt.Sprintf( + "HTTP/1.1 %d %s\n\n", statusCode, http.StatusText(statusCode), + )) + //print("<<<", string(body), "<<<\n") + _, err = w.Write(body) + return +} + +func WriteResponse( + w io.Writer, statusCode int, contentType string, body []byte, +) (err error) { + header := fmt.Sprintf( + "HTTP/1.1 %d %s\nContent-Type: %s\nContent-Length: %d\n\n", + statusCode, http.StatusText(statusCode), contentType, len(body), + ) + body = append([]byte(header), body...) + //print("<<<", string(body), "<<<\n") + _, err = w.Write(body) + return +} + +func WriteChunked(w io.Writer, contentType string, body []byte) (err error) { + header := fmt.Sprintf( + "HTTP/1.1 200 OK\nContent-Type: %s\nTransfer-Encoding: chunked\n\n%x\n", + contentType, len(body), + ) + body = append([]byte(header), body...) + body = append(body, "\n0\n\n"...) + //print("<<<", string(body), "<<<\n") + _, err = w.Write(body) + return +} diff --git a/pkg/homekit/mdns/client.go b/pkg/homekit/mdns/client.go new file mode 100644 index 00000000..c5befa45 --- /dev/null +++ b/pkg/homekit/mdns/client.go @@ -0,0 +1,42 @@ +package mdns + +import ( + "fmt" + "github.com/hashicorp/mdns" + "strings" +) + +const Suffix = "._hap._tcp.local." + +func GetAll() chan *mdns.ServiceEntry { + entries := make(chan *mdns.ServiceEntry) + params := &mdns.QueryParam{ + Service: "_hap._tcp", Entries: entries, DisableIPv6: true, + } + + go func() { + _ = mdns.Query(params) + close(entries) + }() + + return entries +} + +func GetAddress(deviceID string) string { + for entry := range GetAll() { + if strings.Contains(entry.Info, deviceID) { + return fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port) + } + } + + return "" +} + +func GetEntry(deviceID string) *mdns.ServiceEntry { + for entry := range GetAll() { + if strings.Contains(entry.Info, deviceID) { + return entry + } + } + return nil +} diff --git a/pkg/homekit/mdns/server.go b/pkg/homekit/mdns/server.go new file mode 100644 index 00000000..ec78390a --- /dev/null +++ b/pkg/homekit/mdns/server.go @@ -0,0 +1,53 @@ +package mdns + +import ( + "github.com/hashicorp/mdns" + "net" +) + +const HostHeaderTail = "._hap._tcp.local" + +func NewServer(name string, port int, ips []net.IP, txt []string) (*mdns.Server, error) { + if ips == nil || ips[0] == nil { + ips = LocalIPs() + } + + // important to set hostName manually with any value and `.local.` tail + // important to set ips manually + service, _ := mdns.NewMDNSService( + name, "_hap._tcp", "", name+".local.", port, ips, txt, + ) + + return mdns.NewServer(&mdns.Config{Zone: service}) +} + +func LocalIPs() []net.IP { + ifaces, err := net.Interfaces() + if err != nil { + return nil + } + + var ips []net.IP + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 { + continue // interface down + } + if iface.Flags&net.FlagLoopback != 0 { + continue // loopback interface + } + + var addrs []net.Addr + if addrs, err = iface.Addrs(); err != nil { + continue + } + for _, addr := range addrs { + switch addr := addr.(type) { + case *net.IPNet: + ips = append(ips, addr.IP) + case *net.IPAddr: + ips = append(ips, addr.IP) + } + } + } + return ips +} diff --git a/pkg/homekit/pairing.go b/pkg/homekit/pairing.go new file mode 100644 index 00000000..d68886df --- /dev/null +++ b/pkg/homekit/pairing.go @@ -0,0 +1,410 @@ +package homekit + +import ( + "bufio" + "crypto/sha512" + "errors" + "github.com/brutella/hap" + "github.com/brutella/hap/chacha20poly1305" + "github.com/brutella/hap/curve25519" + "github.com/brutella/hap/ed25519" + "github.com/brutella/hap/hkdf" + "github.com/brutella/hap/tlv8" + "github.com/tadglines/go-pkgs/crypto/srp" + "net" + "net/http" +) + +type pairSetupPayload struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + Salt []byte `tlv8:"2"` + PublicKey []byte `tlv8:"3"` + Proof []byte `tlv8:"4"` + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + Error byte `tlv8:"7"` + RetryDelay byte `tlv8:"8"` + Certificate []byte `tlv8:"9"` + Signature []byte `tlv8:"10"` + Permissions byte `tlv8:"11"` + FragmentData []byte `tlv8:"13"` + FragmentLast []byte `tlv8:"14"` +} + +func (s *Server) PairSetupHandler( + conn net.Conn, req *http.Request, +) (clientID string, err error) { + // STEP 1. Request from iPhone + payloadM1 := pairSetupPayload{} + if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil { + return + } + if payloadM1.State != hap.M1 { + err = errors.New("wrong state") + return + } + + // generate our session public and salt using PIN + username := []byte("Pair-Setup") + + var SRP *srp.SRP + if SRP, err = srp.NewSRP( + "rfc5054.3072", sha512.New, + keyDerivativeFuncRFC2945(username), + ); err != nil { + return + } + + SRP.SaltLength = 16 + var salt, verifier []byte + if salt, verifier, err = SRP.ComputeVerifier([]byte(s.Pin)); err != nil { + return + } + session := SRP.NewServerSession(username, salt, verifier) + + // STEP 2. Response to iPhone + payloadM2 := struct { + Salt []byte `tlv8:"2"` + PublicKey []byte `tlv8:"3"` + State byte `tlv8:"6"` + }{ + State: hap.M2, + PublicKey: session.GetB(), + Salt: salt, + } + var buf []byte + if buf, err = tlv8.Marshal(payloadM2); err != nil { + return + } + if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { + return + } + + // STEP 3. Request from iPhone + r := bufio.NewReader(conn) + if req, err = http.ReadRequest(r); err != nil { + return + } + payloadM3 := pairSetupPayload{} + if err = tlv8.UnmarshalReader(req.Body, &payloadM3); err != nil { + return + } + if payloadM3.State != hap.M3 { + err = errors.New("wrong state") + return + } + + // important to compute key before verify client + var sessionShared []byte + if sessionShared, err = session.ComputeKey(payloadM3.PublicKey); err != nil { + return + } + + // support skip pin verify (any pin accepted) + if s.Pin != "" && !session.VerifyClientAuthenticator(payloadM3.Proof) { + err = errors.New("client proof is invalid") + return + } + + serverProof := session.ComputeAuthenticator(payloadM3.Proof) + + // STEP 4. Response to iPhone + payloadM4 := struct { + Proof []byte `tlv8:"4"` + State byte `tlv8:"6"` + }{ + State: hap.M4, Proof: serverProof, + } + if buf, err = tlv8.Marshal(payloadM4); err != nil { + return + } + if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { + return + } + + // STEP 5. Request from iPhone + if req, err = http.ReadRequest(r); err != nil { + return + } + encryptedM5 := pairSetupPayload{} + if err = tlv8.UnmarshalReader(req.Body, &encryptedM5); err != nil { + return + } + if encryptedM5.State != hap.M5 { + err = errors.New("wrong state") + return + } + + msg := encryptedM5.EncryptedData[:len(encryptedM5.EncryptedData)-16] + var mac [16]byte + copy(mac[:], encryptedM5.EncryptedData[len(msg):]) // 16 byte (MAC) + + // decrypt message using session shared + var sessionKey [32]byte + if sessionKey, err = hkdf.Sha512( + sessionShared, []byte("Pair-Setup-Encrypt-Salt"), + []byte("Pair-Setup-Encrypt-Info"), + ); err != nil { + return + } + + if buf, err = chacha20poly1305.DecryptAndVerify( + sessionKey[:], []byte("PS-Msg05"), msg, mac, nil, + ); err != nil { + return + } + + // unpack message from TLV8 + payloadM5 := struct { + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Signature []byte `tlv8:"10"` + }{} + if err = tlv8.Unmarshal(buf, &payloadM5); err != nil { + return + } + + // 3. verify client ID and Public + var saltKey [32]byte + if saltKey, err = hkdf.Sha512( + sessionShared, []byte("Pair-Setup-Controller-Sign-Salt"), + []byte("Pair-Setup-Controller-Sign-Info"), + ); err != nil { + return + } + + buf = nil + buf = append(buf, saltKey[:]...) + buf = append(buf, payloadM5.Identifier...) + buf = append(buf, payloadM5.PublicKey[:]...) + + if !ed25519.ValidateSignature( + payloadM5.PublicKey[:], buf, payloadM5.Signature, + ) { + err = errors.New("wrong client signature") + return + } + + // 4. generate signature to our ID adn Public + if saltKey, err = hkdf.Sha512( + sessionShared, []byte("Pair-Setup-Accessory-Sign-Salt"), + []byte("Pair-Setup-Accessory-Sign-Info"), + ); err != nil { + return + } + + buf = nil + buf = append(buf, saltKey[:]...) + buf = append(buf, []byte(s.ServerID)...) + buf = append(buf, s.ServerPrivate[32:]...) // ServerPublic + + var signature []byte + if signature, err = ed25519.Signature(s.ServerPrivate, buf); err != nil { + return + } + + // 5. pack our ID and Public + payloadM6 := struct { + Identifier []byte `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Signature []byte `tlv8:"10"` + }{ + Identifier: []byte(s.ServerID), + PublicKey: s.ServerPrivate[32:], + Signature: signature, + } + if buf, err = tlv8.Marshal(payloadM6); err != nil { + return + } + + // 6. encrypt message + buf, mac, _ = chacha20poly1305.EncryptAndSeal( + sessionKey[:], []byte("PS-Msg06"), buf, nil, + ) + + // STEP 6. Response to iPhone + encryptedM6 := struct { + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + }{ + State: hap.M6, + EncryptedData: append(buf, mac[:]...), + } + if buf, err = tlv8.Marshal(encryptedM6); err != nil { + return + } + if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { + return + } + + if s.Pairings != nil { + s.Pairings[payloadM5.Identifier] = append( + payloadM5.PublicKey, 1, // adds admin (1) flag + ) + } + + clientID = payloadM5.Identifier + + return +} + +func keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc { + return func(salt, pin []byte) []byte { + h := sha512.New() + h.Write(username) + h.Write([]byte(":")) + h.Write(pin) + t2 := h.Sum(nil) + h.Reset() + h.Write(salt) + h.Write(t2) + return h.Sum(nil) + } +} + +type pairVerifyPayload struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + EncryptedData []byte `tlv8:"5"` + State byte `tlv8:"6"` + Signature []byte `tlv8:"10"` +} + +func (s *Server) PairVerifyHandler( + conn net.Conn, req *http.Request, +) (secure *Secure, err error) { + // STEP M1. Request from iPhone + payloadM1 := pairVerifyPayload{} + if err = tlv8.UnmarshalReader(req.Body, &payloadM1); err != nil { + return + } + if payloadM1.State != hap.M1 { + err = errors.New("wrong state") + return + } + + var clientPublic [32]byte + copy(clientPublic[:], payloadM1.PublicKey) + + // Generate the key pair. + sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() + sessionShared := curve25519.SharedSecret(sessionPrivate, clientPublic) + + var sessionKey [32]byte + if sessionKey, err = hkdf.Sha512( + sessionShared[:], []byte("Pair-Verify-Encrypt-Salt"), + []byte("Pair-Verify-Encrypt-Info"), + ); err != nil { + return + } + + var buf []byte + buf = append(buf, sessionPublic[:]...) + buf = append(buf, s.ServerID...) + buf = append(buf, clientPublic[:]...) + + var signature []byte + if signature, err = ed25519.Signature(s.ServerPrivate[:], buf); err != nil { + return + } + + // STEP M2. Response to iPhone + payloadM2 := struct { + Identifier string `tlv8:"1"` + Signature []byte `tlv8:"10"` + }{ + Identifier: s.ServerID, + Signature: signature, + } + if buf, err = tlv8.Marshal(payloadM2); err != nil { + return + } + + var mac [16]byte + buf, mac, _ = chacha20poly1305.EncryptAndSeal( + sessionKey[:], []byte("PV-Msg02"), buf, nil, + ) + encryptedM2 := struct { + State byte `tlv8:"6"` + PublicKey []byte `tlv8:"3"` + EncryptedData []byte `tlv8:"5"` + }{ + State: hap.M2, + PublicKey: sessionPublic[:], + EncryptedData: append(buf, mac[:]...), + } + if buf, err = tlv8.Marshal(encryptedM2); err != nil { + return + } + if err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf); err != nil { + return + } + + // STEP M3. Request from iPhone + r := bufio.NewReader(conn) + if req, err = http.ReadRequest(r); err != nil { + return + } + encryptedM3 := pairSetupPayload{} + if err = tlv8.UnmarshalReader(req.Body, &encryptedM3); err != nil { + return + } + if encryptedM3.State != hap.M3 { + err = errors.New("wrong state") + return + } + + buf = encryptedM3.EncryptedData[:len(encryptedM3.EncryptedData)-16] + copy(mac[:], encryptedM3.EncryptedData[len(buf):]) // 16 byte (MAC) + + if buf, err = chacha20poly1305.DecryptAndVerify( + sessionKey[:], []byte("PV-Msg03"), buf, mac, nil, + ); err != nil { + return + } + + payloadM3 := pairVerifyPayload{} + if err = tlv8.Unmarshal(buf, &payloadM3); err != nil { + return + } + + if s.Pairings != nil { + pairing := s.Pairings[payloadM3.Identifier] + if pairing == nil { + err = errors.New("not paired yet") + return + } + + buf = nil + buf = append(buf, clientPublic[:]...) + buf = append(buf, []byte(payloadM3.Identifier)...) + buf = append(buf, sessionPublic[:]...) + + if !ed25519.ValidateSignature( + pairing[:32], buf, payloadM3.Signature, + ) { + err = errors.New("signature invalid") + return + } + } + + // STEP M4. Response to iPhone + payloadM4 := struct { + State byte `tlv8:"6"` + }{ + State: hap.M4, + } + if buf, err = tlv8.Marshal(payloadM4); err != nil { + return + } + err = WriteResponse(conn, http.StatusOK, MimeTLV8, buf) + + if secure, err = NewSecure(sessionShared, true); err != nil { + return + } + secure.Conn = conn + + return +} diff --git a/pkg/homekit/secure.go b/pkg/homekit/secure.go new file mode 100644 index 00000000..2370c708 --- /dev/null +++ b/pkg/homekit/secure.go @@ -0,0 +1,137 @@ +package homekit + +import ( + "encoding/binary" + "github.com/brutella/hap/chacha20poly1305" + "github.com/brutella/hap/hkdf" + "net" + "sync" +) + +type Secure struct { + Conn net.Conn + + encryptKey [32]byte + decryptKey [32]byte + encryptCount uint64 + decryptCount uint64 + + mx sync.Mutex +} + +func NewSecure(sharedKey [32]byte, isServer bool) (*Secure, error) { + salt := []byte("Control-Salt") + + key1, err := hkdf.Sha512( + sharedKey[:], salt, []byte("Control-Read-Encryption-Key"), + ) + if err != nil { + return nil, err + } + + key2, err := hkdf.Sha512( + sharedKey[:], salt, []byte("Control-Write-Encryption-Key"), + ) + if err != nil { + return nil, err + } + + if isServer { + return &Secure{encryptKey: key1, decryptKey: key2}, nil + } else { + return &Secure{encryptKey: key2, decryptKey: key1}, nil + } +} + +func (s *Secure) Read(b []byte) (n int, err error) { + for { + var length uint16 + if err = binary.Read(s.Conn, binary.LittleEndian, &length); err != nil { + return + } + + var enc = make([]byte, length) + if err = binary.Read(s.Conn, binary.LittleEndian, &enc); err != nil { + return + } + + var mac [16]byte + if err = binary.Read(s.Conn, binary.LittleEndian, &mac); err != nil { + return + } + + var nonce [8]byte + binary.LittleEndian.PutUint64(nonce[:], s.decryptCount) + s.decryptCount++ + + bLength := make([]byte, 2) + binary.LittleEndian.PutUint16(bLength, length) + + var msg []byte + if msg, err = chacha20poly1305.DecryptAndVerify( + s.decryptKey[:], nonce[:], enc, mac, bLength, + ); err != nil { + return + } + + n += copy(b[n:], msg) + + // Finish when all bytes fit in b + if length < packetLengthMax { + //fmt.Printf(">>>%s>>>\n", b[:n]) + return + } + } +} + +func (s *Secure) Write(b []byte) (n int, err error) { + s.mx.Lock() + defer s.mx.Unlock() + + var packetLen = len(b) + for { + if packetLen > packetLengthMax { + packetLen = packetLengthMax + } + + //fmt.Printf("<<<%s<<<\n", b[:packetLen]) + + var nonce [8]byte + binary.LittleEndian.PutUint64(nonce[:], s.encryptCount) + s.encryptCount++ + + bLength := make([]byte, 2) + binary.LittleEndian.PutUint16(bLength, uint16(packetLen)) + + var enc []byte + var mac [16]byte + enc, mac, err = chacha20poly1305.EncryptAndSeal( + s.encryptKey[:], nonce[:], b[:packetLen], bLength[:], + ) + if err != nil { + return + } + + enc = append(bLength, enc...) + enc = append(enc, mac[:]...) + if _, err = s.Conn.Write(enc); err != nil { + return + } + + n += packetLen + + if packetLen == packetLengthMax { + b = b[packetLengthMax:] + packetLen = len(b) + } else { + break + } + } + + return +} + +const ( + // packetLengthMax is the max length of encrypted packets + packetLengthMax = 0x400 +) diff --git a/pkg/homekit/server.go b/pkg/homekit/server.go new file mode 100644 index 00000000..9cb267c5 --- /dev/null +++ b/pkg/homekit/server.go @@ -0,0 +1,155 @@ +package homekit + +import ( + "bufio" + "crypto/ed25519" + "github.com/brutella/hap" + "github.com/brutella/hap/tlv8" + "io" + "net" + "net/http" +) + +type Server struct { + // Pin can't be null because server proof will be wrong + Pin string `json:"-"` + + ServerID string `json:"server_id"` + // 32 bytes private key + 32 bytes public key + ServerPrivate []byte `json:"server_private"` + + // Pairings can be nil for disable pair verify check + // ClientID: 32 bytes client public + 1 byte (isAdmin) + Pairings map[string][]byte `json:"pairings"` + + DefaultPlainHandler func(w io.Writer, r *http.Request) error + DefaultSecureHandler func(w io.Writer, r *http.Request) error + + OnPairChange func(clientID string, clientPublic []byte) `json:"-"` + OnRequest func(w io.Writer, r *http.Request) `json:"-"` +} + +func GenerateKey() []byte { + _, key, _ := ed25519.GenerateKey(nil) + return key +} + +func NewServer(name string) *Server { + return &Server{ + ServerID: GenerateID(name), + ServerPrivate: GenerateKey(), + Pairings: map[string][]byte{}, + } +} + +func (s *Server) Serve(address string) (err error) { + var ln net.Listener + if ln, err = net.Listen("tcp", address); err != nil { + return + } + + for { + var conn net.Conn + if conn, err = ln.Accept(); err != nil { + continue + } + go func() { + //fmt.Printf("[%s] new connection\n", conn.RemoteAddr().String()) + s.Accept(conn) + //fmt.Printf("[%s] close connection\n", conn.RemoteAddr().String()) + }() + } +} + +func (s *Server) Accept(conn net.Conn) (err error) { + defer conn.Close() + + var req *http.Request + r := bufio.NewReader(conn) + if req, err = http.ReadRequest(r); err != nil { + return + } + + return s.HandleRequest(conn, req) +} + +func (s *Server) HandleRequest(conn net.Conn, req *http.Request) (err error) { + if s.OnRequest != nil { + s.OnRequest(conn, req) + } + + switch req.URL.Path { + case UriPairSetup: + if _, err = s.PairSetupHandler(conn, req); err != nil { + return + } + + case UriPairVerify: + var secure *Secure + if secure, err = s.PairVerifyHandler(conn, req); err != nil { + return + } + + err = s.HandleSecure(secure) + + default: + if s.DefaultPlainHandler != nil { + err = s.DefaultPlainHandler(conn, req) + } + } + + return +} + +func (s *Server) HandleSecure(secure *Secure) (err error) { + r := bufio.NewReader(secure) + for { + var req *http.Request + if req, err = http.ReadRequest(r); err != nil { + return + } + + if s.OnRequest != nil { + s.OnRequest(secure, req) + } + + switch req.URL.Path { + case UriPairings: + s.HandlePairings(secure, req) + default: + if err = s.DefaultSecureHandler(secure, req); err != nil { + return + } + } + } +} + +func (s *Server) HandlePairings(w io.Writer, r *http.Request) { + req := struct { + Method byte `tlv8:"0"` + Identifier string `tlv8:"1"` + PublicKey []byte `tlv8:"3"` + Permission byte `tlv8:"11"` + State byte `tlv8:"6"` + }{} + + if err := tlv8.UnmarshalReader(r.Body, &req); err != nil { + panic(err) + } + + switch req.Method { + case hap.MethodAddPairing, hap.MethodDeletePairing: + res := struct { + State byte `tlv8:"6"` + }{ + State: hap.M2, + } + data, err := tlv8.Marshal(res) + if err != nil { + panic(err) + } + if err = WriteResponse(w, http.StatusOK, MimeJSON, data); err != nil { + panic(err) + } + } +} diff --git a/pkg/srtp/server.go b/pkg/srtp/server.go new file mode 100644 index 00000000..1563095a --- /dev/null +++ b/pkg/srtp/server.go @@ -0,0 +1,61 @@ +package srtp + +import ( + "encoding/binary" + "net" +) + +// Server using same UDP port for SRTP and for SRTCP as the iPhone does +// this is not really necessary but anyway +type Server struct { + sessions map[uint32]*Session +} + +func (s *Server) AddSession(session *Session) { + if s.sessions == nil { + s.sessions = map[uint32]*Session{} + } + s.sessions[session.RemoteSSRC] = session +} + +func (s *Server) RemoveSession(session *Session) { + delete(s.sessions, session.RemoteSSRC) +} + +func (s *Server) Serve(conn net.PacketConn) error { + buf := make([]byte, 2048) + for { + n, addr, err := conn.ReadFrom(buf) + if err != nil { + return err + } + + // Multiplexing RTP Data and Control Packets on a Single Port + // https://datatracker.ietf.org/doc/html/rfc5761 + + // this is default position for SSRC in RTP packet + ssrc := binary.BigEndian.Uint32(buf[8:]) + session, ok := s.sessions[ssrc] + if ok { + if session.Write == nil { + session.Write = func(b []byte) (int, error) { + return conn.WriteTo(b, addr) + } + } + + if err = session.HandleRTP(buf[:n]); err != nil { + return err + } + } else { + // this is default position for SSRC in RTCP packet + ssrc = binary.BigEndian.Uint32(buf[4:]) + if session, ok = s.sessions[ssrc]; !ok { + continue // skip unknown ssrc + } + + if err = session.HandleRTCP(buf[:n]); err != nil { + return err + } + } + } +} diff --git a/pkg/srtp/session.go b/pkg/srtp/session.go new file mode 100644 index 00000000..6c5acbf6 --- /dev/null +++ b/pkg/srtp/session.go @@ -0,0 +1,97 @@ +package srtp + +import ( + "github.com/AlexxIT/go2rtc/pkg/streamer" + "github.com/pion/rtcp" + "github.com/pion/rtp" + "github.com/pion/srtp/v2" +) + +type Session struct { + LocalSSRC uint32 // outgoing SSRC + RemoteSSRC uint32 // incoming SSRC + + localCtx *srtp.Context // write context + remoteCtx *srtp.Context // read context + + Write func(b []byte) (int, error) + Track *streamer.Track +} + +func (s *Session) SetKeys( + localKey, localSalt, remoteKey, remoteSalt []byte, +) (err error) { + if s.localCtx, err = srtp.CreateContext( + localKey, localSalt, GuessProfile(localKey), + ); err != nil { + return + } + s.remoteCtx, err = srtp.CreateContext( + remoteKey, remoteSalt, GuessProfile(remoteKey), + ) + return +} + +func (s *Session) HandleRTP(data []byte) (err error) { + if data, err = s.remoteCtx.DecryptRTP(nil, data, nil); err != nil { + return + } + + packet := &rtp.Packet{} + if err = packet.Unmarshal(data); err != nil { + return + } + + _ = s.Track.WriteRTP(packet) + //s.Output(core.RTP{Channel: s.Channel, Packet: packet}) + + return +} + +func (s *Session) HandleRTCP(data []byte) (err error) { + header := &rtcp.Header{} + if data, err = s.remoteCtx.DecryptRTCP(nil, data, header); err != nil { + return + } + + var packets []rtcp.Packet + if packets, err = rtcp.Unmarshal(data); err != nil { + return + } + + _ = packets + //s.Output(core.RTCP{Channel: s.Channel + 1, Header: header, Packets: packets}) + + if header.Type == rtcp.TypeSenderReport { + err = s.KeepAlive() + } + + return +} + +func (s *Session) KeepAlive() (err error) { + var data []byte + // we can send empty receiver response, but should send it to hold the connection + rep := rtcp.ReceiverReport{SSRC: s.LocalSSRC} + if data, err = rep.Marshal(); err != nil { + return + } + + if data, err = s.localCtx.EncryptRTCP(nil, data, nil); err != nil { + return + } + + _, err = s.Write(data) + + return +} + +func GuessProfile(masterKey []byte) srtp.ProtectionProfile { + switch len(masterKey) { + case 16: + return srtp.ProtectionProfileAes128CmHmacSha1_80 + case 32: + return srtp.ProtectionProfileAes256CmHmacSha1_80 + } + return 0 +} diff --git a/www/devices.html b/www/devices.html index c01c792a..ca9d1bc7 100644 --- a/www/devices.html +++ b/www/devices.html @@ -42,11 +42,8 @@ -
- - add -
- + +
@@ -61,22 +58,18 @@ 0, location.pathname.lastIndexOf("/") ); - function reload() { - fetch(`${baseUrl}/api/devices`).then(r => { - r.json().then(data => { - let html = ''; - - data.forEach(function (item) { - html += ``; - }) - - let content = document.getElementById('devices').getElementsByTagName('tbody')[0]; - content.innerHTML = html - }); + fetch(`${baseUrl}/api/devices`) + .then(r => r.json()) + .then(data => { + document.querySelector("body > table > tbody").innerHTML = + data.reduce((html, item) => { + return html + ` + + + `; + }, ''); }) - } - - reload(); + .catch(console.error); \ No newline at end of file diff --git a/www/homekit.html b/www/homekit.html new file mode 100644 index 00000000..155933bc --- /dev/null +++ b/www/homekit.html @@ -0,0 +1,111 @@ + + + + + + + + go2rtc + + + + + +
+ + +
+
Kind
${item.kind}${item.title}
${item.kind}${item.title}
+ + + + + + + + + + + +
NameAddressModelCommands
+ + + \ No newline at end of file diff --git a/www/index.html b/www/index.html index c5f2bf3a..f905753e 100644 --- a/www/index.html +++ b/www/index.html @@ -42,10 +42,10 @@ +
add - devices
diff --git a/www/main.js b/www/main.js new file mode 100644 index 00000000..f7551d26 --- /dev/null +++ b/www/main.js @@ -0,0 +1,52 @@ +// main menu +document.body.innerHTML = ` + + +` + document.body.innerHTML; diff --git a/www/static.go b/www/static.go index 80e6b169..01f50906 100644 --- a/www/static.go +++ b/www/static.go @@ -3,4 +3,5 @@ package www import "embed" //go:embed *.html +//go:embed *.js var Static embed.FS