diff --git a/internal/api/api.go b/internal/api/api.go index 00619564..02fb9ab8 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -50,7 +50,8 @@ func Init() { HandleFunc("api/exit", exitHandler) // ensure we can listen without errors - listener, err := net.Listen("tcp", cfg.Mod.Listen) + var err error + ln, err = net.Listen("tcp", cfg.Mod.Listen) if err != nil { log.Fatal().Err(err).Msg("[api] listen") return @@ -75,7 +76,7 @@ func Init() { go func() { s := http.Server{} s.Handler = Handler - if err = s.Serve(listener); err != nil { + if err = s.Serve(ln); err != nil { log.Fatal().Err(err).Msg("[api] serve") } }() @@ -111,6 +112,13 @@ func Init() { } } +func Port() int { + if ln == nil { + return 0 + } + return ln.Addr().(*net.TCPAddr).Port +} + const ( MimeJSON = "application/json" MimeText = "text/plain" @@ -192,6 +200,7 @@ func middlewareCORS(next http.Handler) http.Handler { }) } +var ln net.Listener var mu sync.Mutex func apiHandler(w http.ResponseWriter, r *http.Request) { @@ -216,6 +225,7 @@ func exitHandler(w http.ResponseWriter, r *http.Request) { type Source struct { ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` + Info string `json:"info,omitempty"` URL string `json:"url,omitempty"` Location string `json:"location,omitempty"` } @@ -233,3 +243,9 @@ func ResponseSources(w http.ResponseWriter, sources []*Source) { } ResponseJSON(w, response) } + +func Error(w http.ResponseWriter, err error) { + log.Error().Err(err).Caller(1).Send() + + http.Error(w, err.Error(), http.StatusInsufficientStorage) +} diff --git a/internal/app/app.go b/internal/app/app.go index 05177fa8..1aa91527 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,6 +1,7 @@ package app import ( + "errors" "flag" "fmt" "io" @@ -11,9 +12,9 @@ import ( "time" "github.com/AlexxIT/go2rtc/pkg/shell" + "github.com/AlexxIT/go2rtc/pkg/yaml" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "gopkg.in/yaml.v3" ) var Version = "1.6.2" @@ -81,6 +82,8 @@ func Init() { modules = cfg.Mod log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH) + + migrateStore() } func NewLogger(format string, level string) zerolog.Logger { @@ -123,6 +126,22 @@ func GetLogger(module string) zerolog.Logger { return log.Logger } +func PatchConfig(key string, value any, path ...string) error { + if ConfigPath == "" { + return errors.New("config file disabled") + } + + // empty config is OK + b, _ := os.ReadFile(ConfigPath) + + b, err := yaml.Patch(b, key, value, path...) + if err != nil { + return err + } + + return os.WriteFile(ConfigPath, b, 0644) +} + // internal type Config []string diff --git a/internal/app/migrate.go b/internal/app/migrate.go new file mode 100644 index 00000000..95c51c51 --- /dev/null +++ b/internal/app/migrate.go @@ -0,0 +1,35 @@ +package app + +import ( + "encoding/json" + "os" + + "github.com/rs/zerolog/log" +) + +func migrateStore() { + const name = "go2rtc.json" + + data, _ := os.ReadFile(name) + if data == nil { + return + } + + var store struct { + Streams map[string]string `json:"streams"` + } + + if err := json.Unmarshal(data, &store); err != nil { + log.Warn().Err(err).Caller().Send() + return + } + + for id, url := range store.Streams { + if err := PatchConfig(id, url, "streams"); err != nil { + log.Warn().Err(err).Caller().Send() + return + } + } + + _ = os.Remove(name) +} diff --git a/internal/app/store/store.go b/internal/app/store/store.go deleted file mode 100644 index d18de624..00000000 --- a/internal/app/store/store.go +++ /dev/null @@ -1,61 +0,0 @@ -package store - -import ( - "encoding/json" - "github.com/rs/zerolog/log" - "os" -) - -const name = "go2rtc.json" - -var store map[string]any - -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]any) - } -} - -func save() error { - data, err := json.Marshal(store) - if err != nil { - return err - } - - return os.WriteFile(name, data, 0644) -} - -func GetRaw(key string) any { - if store == nil { - load() - } - - return store[key] -} - -func GetDict(key string) map[string]any { - raw := GetRaw(key) - if raw != nil { - return raw.(map[string]any) - } - - return make(map[string]any) -} - -func Set(key string, v any) error { - if store == nil { - load() - } - - store[key] = v - - return save() -} diff --git a/internal/homekit/api.go b/internal/homekit/api.go index ee14ef4a..42ea26ec 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -1,12 +1,14 @@ package homekit import ( + "errors" + "fmt" "net/http" "net/url" "strings" "github.com/AlexxIT/go2rtc/internal/api" - "github.com/AlexxIT/go2rtc/internal/app/store" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/mdns" @@ -15,119 +17,122 @@ import ( func apiHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case "GET": - items := make([]any, 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) - } - } - - err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { - log.Trace().Msgf("[homekit] mdns=%s", entry) - - if entry.Complete() { - device := Device{ - Name: entry.Name, - Addr: entry.Addr(), - ID: entry.Info[hap.TXTDeviceID], - Model: entry.Info[hap.TXTModel], - Paired: entry.Info[hap.TXTStatusFlags] == "0", - } - items = append(items, device) - } - return false - }) - + sources, err := discovery() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + api.Error(w, err) return } - api.ResponseJSON(w, items) + 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": - // TODO: post params... + if err := r.ParseMultipartForm(1024); err != nil { + api.Error(w, err) + return + } - id := r.URL.Query().Get("id") - pin := r.URL.Query().Get("pin") - name := r.URL.Query().Get("name") - if err := hkPair(id, pin, name); err != nil { - log.Error().Err(err).Caller().Send() - http.Error(w, err.Error(), http.StatusInternalServerError) + if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil { + api.Error(w, err) } case "DELETE": - src := r.URL.Query().Get("src") - if err := hkDelete(src); err != nil { - log.Error().Err(err).Caller().Send() - http.Error(w, err.Error(), http.StatusInternalServerError) + 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) } } } -func hkPair(deviceID, pin, name string) (err error) { - var conn *hap.Client +func discovery() ([]*api.Source, error) { + var sources []*api.Source - if conn, err = hap.Pair(deviceID, pin); err != nil { - return + // 1. Get streams from Discovery + err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { + log.Trace().Msgf("[homekit] mdns=%s", entry) + + if entry.Complete() && entry.Info[hap.TXTCategory] == hap.CategoryCamera { + source := &api.Source{ + Name: entry.Name, + Info: entry.Info[hap.TXTModel], + URL: fmt.Sprintf( + "homekit://%s:%d?device_id=%s&feature=%s&status=%s", + entry.IP, entry.Port, entry.Info[hap.TXTDeviceID], + entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags], + ), + } + + sources = append(sources, source) + } + return false + }) + + if err != nil { + return nil, err } - streams.New(name, conn.URL()) - - dict := store.GetDict("streams") - dict[name] = conn.URL() - - return store.Set("streams", dict) + return sources, nil } -func hkDelete(name string) (err error) { - dict := store.GetDict("streams") - for key, rawURL := range dict { - if key != name { - continue - } - - var conn *hap.Client - - if conn, err = hap.NewClient(rawURL.(string)); err != nil { - return - } - - if err = conn.Dial(); err != nil { - return - } - - if err = conn.ListPairings(); err != nil { - return - } - - if err = conn.DeletePairing(conn.ClientID); err != nil { - log.Error().Err(err).Caller().Send() - } - - delete(dict, name) - - return store.Set("streams", dict) +func apiPair(id, url string) error { + conn, err := hap.Pair(url) + if err != nil { + return err } - return nil + streams.New(id, conn.URL()) + + return app.PatchConfig(id, conn.URL(), "streams") } -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"` +func apiUnpair(id string) error { + stream := streams.Get(id) + if stream == nil { + return errors.New(api.StreamNotFound) + } + + rawURL := findHomeKitURL(stream) + if rawURL == "" { + return errors.New("not homekit source") + } + + if err := hap.Unpair(rawURL); err != nil { + return err + } + + streams.Delete(id) + + return app.PatchConfig(id, nil, "streams") +} + +func findHomeKitURLs() map[string]*url.URL { + urls := map[string]*url.URL{} + for id, stream := range streams.Streams() { + if rawURL := findHomeKitURL(stream); rawURL != "" { + if u, err := url.Parse(rawURL); err == nil { + urls[id] = u + } + } + } + return urls } diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index 8c81bcb4..0e2c60e1 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -1,6 +1,8 @@ package homekit import ( + "strings" + "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/srtp" @@ -23,3 +25,24 @@ var log zerolog.Logger func streamHandler(url string) (core.Producer, error) { return homekit.Dial(url, srtp.Server) } + +func findHomeKitURL(stream *streams.Stream) string { + sources := stream.Sources() + if len(sources) == 0 { + return "" + } + + url := sources[0] + if strings.HasPrefix(url, "homekit") { + return url + } + + if strings.HasPrefix(url, "hass") { + location, _ := streams.Location(url) + if strings.HasPrefix(location, "homekit") { + return url + } + } + + return "" +} diff --git a/internal/streams/stream.go b/internal/streams/stream.go index d80c13a9..c918aaba 100644 --- a/internal/streams/stream.go +++ b/internal/streams/stream.go @@ -38,6 +38,13 @@ func NewStream(source any) *Stream { } } +func (s *Stream) Sources() (sources []string) { + for _, prod := range s.producers { + sources = append(sources, prod.url) + } + return +} + func (s *Stream) SetSource(source string) { for _, prod := range s.producers { prod.SetSource(source) diff --git a/internal/streams/streams.go b/internal/streams/streams.go index e674237d..bc23bf54 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -8,7 +8,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" - "github.com/AlexxIT/go2rtc/internal/app/store" "github.com/rs/zerolog" ) @@ -25,10 +24,6 @@ func Init() { streams[name] = NewStream(item) } - for name, item := range store.GetDict("streams") { - streams[name] = NewStream(item) - } - api.HandleFunc("api/streams", streamsHandler) } @@ -118,6 +113,14 @@ func GetAll() (names []string) { return } +func Streams() map[string]*Stream { + return streams +} + +func Delete(id string) { + delete(streams, id) +} + func streamsHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() src := query.Get("src") @@ -141,6 +144,11 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) { if New(name, src) == nil { http.Error(w, "", http.StatusBadRequest) + return + } + + if err := app.PatchConfig(name, src, "streams"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) } case "PATCH": @@ -173,6 +181,10 @@ func streamsHandler(w http.ResponseWriter, r *http.Request) { case "DELETE": delete(streams, src) + + if err := app.PatchConfig(src, nil, "streams"); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } } } diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go index bb114391..7cb10b24 100644 --- a/pkg/hap/client_pairing.go +++ b/pkg/hap/client_pairing.go @@ -5,43 +5,74 @@ import ( "crypto/sha512" "errors" "net" + "net/url" "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/AlexxIT/go2rtc/pkg/mdns" "github.com/tadglines/go-pkgs/crypto/srp" ) -func Pair(deviceID, pin string) (*Client, error) { - var addr string - var mfi bool - - _ = mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool { - if entry.Complete() && entry.Info[TXTDeviceID] == deviceID { - addr = entry.Addr() - mfi = entry.Info[TXTFeatureFlags] == "1" - return true - } - return false - }) - - if addr == "" { - return nil, errors.New("hap: mdns.Discovery") +// Pair homekit +func Pair(rawURL string) (*Client, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, err } + query := u.Query() + c := &Client{ - DeviceAddress: addr, - DeviceID: deviceID, - ClientID: GenerateUUID(), - ClientPrivate: GenerateKey(), + DeviceAddress: u.Host, + DeviceID: query.Get("device_id"), + ClientID: query.Get("client_id"), + ClientPrivate: DecodeKey(query.Get("client_private")), } - return c, c.Pair(mfi, pin) + if c.ClientID == "" { + c.ClientID = GenerateUUID() + } + if c.ClientPrivate == nil { + c.ClientPrivate = GenerateKey() + } + + if err = c.Pair(query.Get("feature"), query.Get("pin")); err != nil { + return nil, err + } + + return c, nil } -func (c *Client) Pair(mfi bool, pin string) (err error) { +func Unpair(rawURL string) error { + u, err := url.Parse(rawURL) + if err != nil { + return err + } + + query := u.Query() + conn := &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")), + } + + if err = conn.Dial(); err != nil { + return err + } + + defer conn.Close() + + if err = conn.ListPairings(); err != nil { + return err + } + + return conn.DeletePairing(conn.ClientID) +} + +func (c *Client) Pair(feature, pin string) (err error) { if pin, err = SanitizePin(pin); err != nil { return err } @@ -61,7 +92,7 @@ func (c *Client) Pair(mfi bool, pin string) (err error) { Method: MethodPair, State: StateM1, } - if mfi { + if feature == "1" { plainM1.Method = MethodPairMFi // ff=1 => method=1, ff=2 => method=0 } res, err := c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM1)) diff --git a/pkg/yaml/yaml_test.go b/pkg/yaml/yaml_test.go index 7547e19b..4b3d8dfe 100644 --- a/pkg/yaml/yaml_test.go +++ b/pkg/yaml/yaml_test.go @@ -9,6 +9,7 @@ import ( func TestPatch(t *testing.T) { b := []byte(`# prefix`) + // 1. Add first b, err := Patch(b, "camera1", "url1", "streams") require.Nil(t, err) @@ -17,6 +18,7 @@ streams: camera1: url1 `, string(b)) + // 2. Add second b, err = Patch(b, "camera2", []string{"url2", "url3"}, "streams") require.Nil(t, err) @@ -28,6 +30,7 @@ streams: - url3 `, string(b)) + // 3. Replace first b, err = Patch(b, "camera1", "url4", "streams") require.Nil(t, err) @@ -39,6 +42,7 @@ streams: - url3 `, string(b)) + // 4. Replace second b, err = Patch(b, "camera2", "url5", "streams") require.Nil(t, err) @@ -48,6 +52,7 @@ streams: camera2: url5 `, string(b)) + // 5. Delete first b, err = Patch(b, "camera1", nil, "streams") require.Nil(t, err) @@ -65,10 +70,8 @@ streams: camera1: url1 `) - pairings := map[string]string{ - "client1": "public1", - "client2": "public2", - } + // 1. Add new key + pairings := []string{"client1", "client2"} b, err := Patch(b, "pairings", pairings, "homekit", "camera1") require.Nil(t, err) @@ -77,8 +80,8 @@ streams: camera1: pin: 123-45-678 pairings: - client1: public1 - client2: public2 + - client1 + - client2 streams: camera1: url1 `, string(b)) diff --git a/www/add.html b/www/add.html index 7648eed3..53665777 100644 --- a/www/add.html +++ b/www/add.html @@ -59,7 +59,18 @@
@@ -104,76 +106,79 @@ url.searchParams.set('src', ev.target.elements['src'].value); const r = await fetch(url, {method: 'PUT'}); - alert(r.ok ? 'OK' : 'ERROR'); + alert(r.ok ? 'OK' : 'ERROR: ' + await r.text()); });| Name | -Address | -Model | -Commands | -
|---|