diff --git a/README.md b/README.md index 304b04c4..f27afeb0 100644 --- a/README.md +++ b/README.md @@ -919,6 +919,7 @@ api: listen: ":1984" # default ":1984", HTTP API port ("" - disabled) username: "admin" # default "", Basic auth for WebUI password: "pass" # default "", Basic auth for WebUI + local_auth: true # default false, Enable auth check for localhost requests base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api) static_dir: "www" # default "", folder for static files (custom web interface) origin: "*" # default "", allow CORS requests (only * supported) @@ -1241,6 +1242,27 @@ log: ## Security +> [!IMPORTANT] +> If an attacker gains access to the API, you are in danger. Through the API, an attacker can use insecure sources such as echo and exec. And get full access to your server. + +For maximum (paranoid) security, go2rtc has special settings: + +```yaml +app: + # use only allowed modules + modules: [api, rtsp, webrtc, exec, ffmpeg, mjpeg] + +api: + # use only allowed API paths + allow_paths: [/api, /api/streams, /api/webrtc, /api/frame.jpeg] + # enable auth for localhost (used together with username and password) + local_auth: true + +exec: + # use only allowed exec paths + allow_paths: [ffmpeg] +``` + By default, `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as uses port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant Add-on. This is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config: diff --git a/internal/api/api.go b/internal/api/api.go index 419e2bdf..dfb65117 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "os" + "slices" "strconv" "strings" "sync" @@ -23,6 +24,7 @@ func Init() { Listen string `yaml:"listen"` Username string `yaml:"username"` Password string `yaml:"password"` + LocalAuth bool `yaml:"local_auth"` BasePath string `yaml:"base_path"` StaticDir string `yaml:"static_dir"` Origin string `yaml:"origin"` @@ -30,6 +32,8 @@ func Init() { TLSCert string `yaml:"tls_cert"` TLSKey string `yaml:"tls_key"` UnixListen string `yaml:"unix_listen"` + + AllowPaths []string `yaml:"allow_paths"` } `yaml:"api"` } @@ -43,6 +47,7 @@ func Init() { return } + allowPaths = cfg.Mod.AllowPaths basePath = cfg.Mod.BasePath log = app.GetLogger("api") @@ -61,7 +66,7 @@ func Init() { } if cfg.Mod.Username != "" { - Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd + Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd } if log.Trace().Enabled() { @@ -152,6 +157,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) { if len(pattern) == 0 || pattern[0] != '/' { pattern = basePath + "/" + pattern } + if allowPaths != nil && !slices.Contains(allowPaths, pattern) { + log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths") + return + } log.Trace().Str("path", pattern).Msg("[api] register path") http.HandleFunc(pattern, handler) } @@ -185,6 +194,7 @@ func Response(w http.ResponseWriter, body any, contentType string) { const StreamNotFound = "stream not found" +var allowPaths []string var basePath string var log zerolog.Logger @@ -195,9 +205,13 @@ func middlewareLog(next http.Handler) http.Handler { }) } -func middlewareAuth(username, password string, next http.Handler) http.Handler { +func isLoopback(remoteAddr string) bool { + return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@" +} + +func middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" { + if localAuth || !isLoopback(r.RemoteAddr) { user, pass, ok := r.BasicAuth() if !ok || user != username || pass != password { w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`) diff --git a/internal/app/app.go b/internal/app/app.go index eb803584..6fed0194 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -11,6 +11,7 @@ import ( var ( Version string + Modules []string UserAgent string ConfigPath string Info = make(map[string]any) @@ -76,6 +77,16 @@ func Init() { if ConfigPath != "" { Logger.Info().Str("path", ConfigPath).Msg("config") } + + var cfg struct { + Mod struct { + Modules []string `yaml:"modules"` + } `yaml:"app"` + } + + LoadConfig(&cfg) + + Modules = cfg.Mod.Modules } func readRevisionTime() (revision, vcsTime string) { diff --git a/internal/echo/echo.go b/internal/echo/echo.go index fb105cec..f33982fa 100644 --- a/internal/echo/echo.go +++ b/internal/echo/echo.go @@ -2,7 +2,9 @@ package echo import ( "bytes" + "errors" "os/exec" + "slices" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" @@ -10,11 +12,25 @@ import ( ) func Init() { + var cfg struct { + Mod struct { + AllowPaths []string `yaml:"allow_paths"` + } `yaml:"echo"` + } + + app.LoadConfig(&cfg) + + allowPaths := cfg.Mod.AllowPaths + log := app.GetLogger("echo") streams.RedirectFunc("echo", func(url string) (string, error) { args := shell.QuoteSplit(url[5:]) + if allowPaths != nil && !slices.Contains(allowPaths, args[0]) { + return "", errors.New("echo: bin not in allow_paths: " + args[0]) + } + b, err := exec.Command(args[0], args[1:]...).Output() if err != nil { return "", err @@ -26,4 +42,5 @@ func Init() { return string(b), nil }) + streams.MarkInsecure("echo") } diff --git a/internal/exec/exec.go b/internal/exec/exec.go index 711be8a2..bf99168f 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -9,6 +9,7 @@ import ( "io" "net/url" "os" + "slices" "strings" "sync" "syscall" @@ -26,6 +27,16 @@ import ( ) func Init() { + var cfg struct { + Mod struct { + AllowPaths []string `yaml:"allow_paths"` + } `yaml:"exec"` + } + + app.LoadConfig(&cfg) + + allowPaths = cfg.Mod.AllowPaths + rtsp.HandleFunc(func(conn *pkg.Conn) bool { waitersMu.Lock() waiter := waiters[conn.URL.Path] @@ -45,10 +56,13 @@ func Init() { }) streams.HandleFunc("exec", execHandle) + streams.MarkInsecure("exec") log = app.GetLogger("exec") } +var allowPaths []string + func execHandle(rawURL string) (prod core.Producer, err error) { rawURL, rawQuery, _ := strings.Cut(rawURL, "#") query := streams.ParseQuery(rawQuery) @@ -73,6 +87,10 @@ func execHandle(rawURL string) (prod core.Producer, err error) { debug: log.Debug().Enabled(), } + if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) { + return nil, errors.New("exec: bin not in allow_paths: " + cmd.Args[0]) + } + if s := query.Get("killsignal"); s != "" { sig := syscall.Signal(core.Atoi(s)) cmd.Cancel = func() error { diff --git a/internal/expr/expr.go b/internal/expr/expr.go index 8fd6c9c2..60d32a84 100644 --- a/internal/expr/expr.go +++ b/internal/expr/expr.go @@ -25,4 +25,5 @@ func Init() { return url, nil }) + streams.MarkInsecure("expr") } diff --git a/internal/hass/api.go b/internal/hass/api.go index e3de23b3..9f110fc8 100644 --- a/internal/hass/api.go +++ b/internal/hass/api.go @@ -30,10 +30,10 @@ func apiStream(w http.ResponseWriter, r *http.Request) { // 1. link to go2rtc stream: rtsp://...:8554/{stream_name} // 2. static link to Hass camera // 3. dynamic link to Hass camera - if streams.Patch(v.Name, v.Channels.First.Url) != nil { + if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil { apiOK(w, r) } else { - http.Error(w, "", http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusBadRequest) } // /stream/{id}/channel/0/webrtc diff --git a/internal/hass/hass.go b/internal/hass/hass.go index ea172b02..99c63692 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "path" + "strings" "sync" "github.com/AlexxIT/go2rtc/internal/api" @@ -37,8 +38,13 @@ func Init() { api.HandleFunc("/streams", apiOK) api.HandleFunc("/stream/", apiStream) - streams.RedirectFunc("hass", func(url string) (string, error) { - if location := entities[url[5:]]; location != "" { + streams.RedirectFunc("hass", func(rawURL string) (string, error) { + rawURL, rawQuery, _ := strings.Cut(rawURL, "#") + + if location := entities[rawURL[5:]]; location != "" { + if rawQuery != "" { + return location + "#" + rawQuery, nil + } return location, nil } diff --git a/internal/hls/ws.go b/internal/hls/ws.go index 608f515f..00eedfe2 100644 --- a/internal/hls/ws.go +++ b/internal/hls/ws.go @@ -11,7 +11,7 @@ import ( ) func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } diff --git a/internal/homekit/api.go b/internal/homekit/api.go index 9f76c2d6..885a40fa 100644 --- a/internal/homekit/api.go +++ b/internal/homekit/api.go @@ -3,6 +3,7 @@ package homekit import ( "errors" "fmt" + "io" "net/http" "net/url" "strings" @@ -14,56 +15,97 @@ import ( "github.com/AlexxIT/go2rtc/pkg/mdns" ) -func apiHandler(w http.ResponseWriter, r *http.Request) { +func apiDiscovery(w http.ResponseWriter, r *http.Request) { + sources, err := discovery() + if err != nil { + api.Error(w, err) + return + } + + urls := findHomeKitURLs() + for id, u := range urls { + deviceID := u.Query().Get("device_id") + for _, source := range sources { + if strings.Contains(source.URL, deviceID) { + source.Location = id + break + } + } + } + + for _, source := range sources { + if source.Location == "" { + source.Location = " " + } + } + + api.ResponseSources(w, sources) +} + +func apiHomekit(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + switch r.Method { case "GET": - sources, err := discovery() - if err != nil { - api.Error(w, err) - return - } - - 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 - } + if id := r.Form.Get("id"); id != "" { + if srv := servers[id]; srv != nil { + api.ResponsePrettyJSON(w, srv) + } else { + http.Error(w, "server not found", http.StatusNotFound) } + } else { + api.ResponsePrettyJSON(w, servers) } - for _, source := range sources { - if source.Location == "" { - source.Location = " " - } - } - - api.ResponseSources(w, sources) - case "POST": - if err := r.ParseMultipartForm(1024); err != nil { - api.Error(w, err) - return - } - - if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil { - api.Error(w, err) + id := r.Form.Get("id") + rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin") + if err := apiPair(id, rawURL); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } case "DELETE": - if err := r.ParseMultipartForm(1024); err != nil { - api.Error(w, err) - return - } - - if err := apiUnpair(r.Form.Get("id")); err != nil { - api.Error(w, err) + id := r.Form.Get("id") + if err := apiUnpair(id); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } } } +func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + stream := streams.Get(id) + if stream == nil { + http.Error(w, "", http.StatusNotFound) + return + } + + rawURL := findHomeKitURL(stream.Sources()) + if rawURL == "" { + http.Error(w, "", http.StatusBadRequest) + return + } + + client, err := hap.Dial(rawURL) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer client.Close() + + res, err := client.Get(hap.PathAccessories) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", api.MimeJSON) + _, _ = io.Copy(w, res.Body) +} + func discovery() ([]*api.Source, error) { var sources []*api.Source diff --git a/internal/homekit/homekit.go b/internal/homekit/homekit.go index b4237211..59b84b3b 100644 --- a/internal/homekit/homekit.go +++ b/internal/homekit/homekit.go @@ -2,8 +2,6 @@ package homekit import ( "errors" - "io" - "net" "net/http" "strings" @@ -26,6 +24,7 @@ func Init() { Name string `yaml:"name"` DeviceID string `yaml:"device_id"` DevicePrivate string `yaml:"device_private"` + CategoryID string `yaml:"category_id"` Pairings []string `yaml:"pairings"` } `yaml:"homekit"` } @@ -35,12 +34,15 @@ func Init() { streams.HandleFunc("homekit", streamHandler) - api.HandleFunc("api/homekit", apiHandler) + api.HandleFunc("api/homekit", apiHomekit) + api.HandleFunc("api/homekit/accessories", apiHomekitAccessories) + api.HandleFunc("api/discovery/homekit", apiDiscovery) if cfg.Mod == nil { return } + hosts = map[string]*server{} servers = map[string]*server{} var entries []*mdns.ServiceEntry @@ -63,36 +65,19 @@ func Init() { deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address name := calcName(conf.Name, deviceID) + setupID := calcSetupID(id) srv := &server{ stream: id, - srtp: srtp.Server, pairings: conf.Pairings, + setupID: setupID, } srv.hap = &hap.Server{ - Pin: pin, - DeviceID: deviceID, - DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id), - GetPair: srv.GetPair, - AddPair: srv.AddPair, - Handler: homekit.ServerHandler(srv), - } - - if url := findHomeKitURL(stream.Sources()); url != "" { - // 1. Act as transparent proxy for HomeKit camera - dial := func() (net.Conn, error) { - client, err := homekit.Dial(url, srtp.Server) - if err != nil { - return nil, err - } - return client.Conn(), nil - } - srv.hap.Handler = homekit.ProxyHandler(srv, dial) - } else { - // 2. Act as basic HomeKit camera - srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version) - srv.hap.Handler = homekit.ServerHandler(srv) + Pin: pin, + DeviceID: deviceID, + DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id), + GetClientPublic: srv.GetPair, } srv.mdns = &mdns.ServiceEntry{ @@ -106,23 +91,32 @@ func Init() { hap.TXTProtoVersion: "1.1", hap.TXTStateNumber: "1", hap.TXTStatusFlags: hap.StatusNotPaired, - hap.TXTCategory: hap.CategoryCamera, - hap.TXTSetupHash: srv.hap.SetupHash(), + hap.TXTCategory: calcCategoryID(conf.CategoryID), + hap.TXTSetupHash: hap.SetupHash(setupID, deviceID), }, } entries = append(entries, srv.mdns) srv.UpdateStatus() + if url := findHomeKitURL(stream.Sources()); url != "" { + // 1. Act as transparent proxy for HomeKit camera + srv.proxyURL = url + } else { + // 2. Act as basic HomeKit camera + srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version) + } + host := srv.mdns.Host(mdns.ServiceHAP) - servers[host] = srv + hosts[host] = srv + servers[id] = srv + + log.Trace().Msgf("[homekit] new server: %s", srv.mdns) } api.HandleFunc(hap.PathPairSetup, hapHandler) api.HandleFunc(hap.PathPairVerify, hapHandler) - log.Trace().Msgf("[homekit] mdns: %s", entries) - go func() { if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil { log.Error().Err(err).Caller().Send() @@ -131,6 +125,7 @@ func Init() { } var log zerolog.Logger +var hosts map[string]*server var servers map[string]*server func streamHandler(rawURL string) (core.Producer, error) { @@ -142,6 +137,8 @@ func streamHandler(rawURL string) (core.Producer, error) { client, err := homekit.Dial(rawURL, srtp.Server) if client != nil && rawQuery != "" { query := streams.ParseQuery(rawQuery) + client.MaxWidth = core.Atoi(query.Get("maxwidth")) + client.MaxHeight = core.Atoi(query.Get("maxheight")) client.Bitrate = parseBitrate(query.Get("bitrate")) } @@ -149,45 +146,27 @@ func streamHandler(rawURL string) (core.Producer, error) { } func resolve(host string) *server { - if len(servers) == 1 { - for _, srv := range servers { + if len(hosts) == 1 { + for _, srv := range hosts { return srv } } - if srv, ok := servers[host]; ok { + if srv, ok := hosts[host]; ok { return srv } return nil } func hapHandler(w http.ResponseWriter, r *http.Request) { - conn, rw, err := w.(http.Hijacker).Hijack() - if err != nil { - return - } - - defer conn.Close() - // Can support multiple HomeKit cameras on single port ONLY for Apple devices. // Doesn't support Home Assistant and any other open source projects // because they don't send the host header in requests. srv := resolve(r.Host) if srv == nil { log.Error().Msg("[homekit] unknown host: " + r.Host) - _ = hap.WriteBackoff(rw) return } - - switch r.RequestURI { - case hap.PathPairSetup: - err = srv.hap.PairSetup(r, rw, conn) - case hap.PathPairVerify: - err = srv.hap.PairVerify(r, rw, conn) - } - - if err != nil && err != io.EOF { - log.Error().Err(err).Caller().Send() - } + srv.Handle(w, r) } func findHomeKitURL(sources []string) string { @@ -203,7 +182,7 @@ func findHomeKitURL(sources []string) string { if strings.HasPrefix(url, "hass") { location, _ := streams.Location(url) if strings.HasPrefix(location, "homekit") { - return url + return location } } diff --git a/internal/homekit/server.go b/internal/homekit/server.go index 6c8b37ae..86cfbc15 100644 --- a/internal/homekit/server.go +++ b/internal/homekit/server.go @@ -4,10 +4,16 @@ import ( "crypto/ed25519" "crypto/sha512" "encoding/hex" + "encoding/json" + "errors" "fmt" + "io" "net" + "net/http" "net/url" + "slices" "strings" + "sync" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/ffmpeg" @@ -16,23 +22,142 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/camera" + "github.com/AlexxIT/go2rtc/pkg/hap/hds" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" "github.com/AlexxIT/go2rtc/pkg/homekit" "github.com/AlexxIT/go2rtc/pkg/magic" "github.com/AlexxIT/go2rtc/pkg/mdns" - "github.com/AlexxIT/go2rtc/pkg/srtp" ) type server struct { - stream string // stream name from YAML - hap *hap.Server // server for HAP connection and encryption - mdns *mdns.ServiceEntry - srtp *srtp.Server - accessory *hap.Accessory // HAP accessory - pairings []string // pairings list + hap *hap.Server // server for HAP connection and encryption + mdns *mdns.ServiceEntry - streams map[string]*homekit.Consumer - consumer *homekit.Consumer + pairings []string // pairings list + conns []any + mu sync.Mutex + + accessory *hap.Accessory // HAP accessory + consumer *homekit.Consumer + proxyURL string + setupID string + stream string // stream name from YAML +} + +func (s *server) MarshalJSON() ([]byte, error) { + v := struct { + Name string `json:"name"` + DeviceID string `json:"device_id"` + Paired int `json:"paired,omitempty"` + CategoryID string `json:"category_id,omitempty"` + SetupCode string `json:"setup_code,omitempty"` + SetupID string `json:"setup_id,omitempty"` + Conns []any `json:"connections,omitempty"` + }{ + Name: s.mdns.Name, + DeviceID: s.mdns.Info[hap.TXTDeviceID], + CategoryID: s.mdns.Info[hap.TXTCategory], + Paired: len(s.pairings), + Conns: s.conns, + } + if v.Paired == 0 { + v.SetupCode = s.hap.Pin + v.SetupID = s.setupID + } + return json.Marshal(v) +} + +func (s *server) Handle(w http.ResponseWriter, r *http.Request) { + conn, rw, err := w.(http.Hijacker).Hijack() + if err != nil { + return + } + + defer conn.Close() + + // Fix reading from Body after Hijack. + r.Body = io.NopCloser(rw) + + switch r.RequestURI { + case hap.PathPairSetup: + id, key, err := s.hap.PairSetup(r, rw) + if err != nil { + log.Error().Err(err).Caller().Send() + return + } + + s.AddPair(id, key, hap.PermissionAdmin) + + case hap.PathPairVerify: + id, key, err := s.hap.PairVerify(r, rw) + if err != nil { + log.Debug().Err(err).Caller().Send() + return + } + + log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr()) + + controller, err := hap.NewConn(conn, rw, key, false) + if err != nil { + log.Error().Err(err).Caller().Send() + return + } + + s.AddConn(controller) + defer s.DelConn(controller) + + var handler homekit.HandlerFunc + + switch { + case s.accessory != nil: + handler = homekit.ServerHandler(s) + case s.proxyURL != "": + client, err := hap.Dial(s.proxyURL) + if err != nil { + log.Error().Err(err).Caller().Send() + return + } + handler = homekit.ProxyHandler(s, client.Conn) + } + + // If your iPhone goes to sleep, it will be an EOF error. + if err = handler(controller); err != nil && !errors.Is(err, io.EOF) { + log.Error().Err(err).Caller().Send() + return + } + } +} + +type logger struct { + v any +} + +func (l logger) String() string { + switch v := l.v.(type) { + case *hap.Conn: + return "hap " + v.RemoteAddr().String() + case *hds.Conn: + return "hds " + v.RemoteAddr().String() + case *homekit.Consumer: + return "rtp " + v.RemoteAddr + } + return "unknown" +} + +func (s *server) AddConn(v any) { + log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v}) + s.mu.Lock() + s.conns = append(s.conns, v) + s.mu.Unlock() +} + +func (s *server) DelConn(v any) { + log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v}) + s.mu.Lock() + if i := slices.Index(s.conns, v); i >= 0 { + s.conns = slices.Delete(s.conns, i, i+1) + } + s.mu.Unlock() } func (s *server) UpdateStatus() { @@ -44,12 +169,68 @@ func (s *server) UpdateStatus() { } } +func (s *server) pairIndex(id string) int { + id = "client_id=" + id + for i, pairing := range s.pairings { + if strings.HasPrefix(pairing, id) { + return i + } + } + return -1 +} + +func (s *server) GetPair(id string) []byte { + s.mu.Lock() + defer s.mu.Unlock() + + if i := s.pairIndex(id); i >= 0 { + query, _ := url.ParseQuery(s.pairings[i]) + b, _ := hex.DecodeString(query.Get("client_public")) + return b + } + return nil +} + +func (s *server) AddPair(id string, public []byte, permissions byte) { + log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions) + + s.mu.Lock() + if s.pairIndex(id) < 0 { + s.pairings = append(s.pairings, fmt.Sprintf( + "client_id=%s&client_public=%x&permissions=%d", id, public, permissions, + )) + s.UpdateStatus() + s.PatchConfig() + } + s.mu.Unlock() +} + +func (s *server) DelPair(id string) { + log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id) + + s.mu.Lock() + if i := s.pairIndex(id); i >= 0 { + s.pairings = append(s.pairings[:i], s.pairings[i+1:]...) + s.UpdateStatus() + s.PatchConfig() + } + s.mu.Unlock() +} + +func (s *server) PatchConfig() { + if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil { + log.Error().Err(err).Msgf( + "[homekit] can't save %s pairings=%v", s.stream, s.pairings, + ) + } +} + func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory { return []*hap.Accessory{s.accessory} } func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { - log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid) + log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid) char := s.accessory.GetCharacterByID(iid) if char == nil { @@ -59,11 +240,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { switch char.Type { case camera.TypeSetupEndpoints: - if s.consumer == nil { + consumer := s.consumer + if consumer == nil { return nil } - answer := s.consumer.GetAnswer() + answer := consumer.GetAnswer() v, err := tlv8.MarshalBase64(answer) if err != nil { return nil @@ -76,7 +258,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any { } func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) { - log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value) + log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value) char := s.accessory.GetCharacterByID(iid) if char == nil { @@ -86,61 +268,64 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a switch char.Type { case camera.TypeSetupEndpoints: - var offer camera.SetupEndpoints + var offer camera.SetupEndpointsRequest if err := tlv8.UnmarshalBase64(value, &offer); err != nil { return } - s.consumer = homekit.NewConsumer(conn, srtp2.Server) - s.consumer.SetOffer(&offer) + consumer := homekit.NewConsumer(conn, srtp2.Server) + consumer.SetOffer(&offer) + s.consumer = consumer case camera.TypeSelectedStreamConfiguration: - var conf camera.SelectedStreamConfig + var conf camera.SelectedStreamConfiguration if err := tlv8.UnmarshalBase64(value, &conf); err != nil { return } - log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command) + log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command) switch conf.Control.Command { case camera.SessionCommandEnd: - if consumer := s.streams[conf.Control.SessionID]; consumer != nil { - _ = consumer.Stop() + for _, consumer := range s.conns { + if consumer, ok := consumer.(*homekit.Consumer); ok { + if consumer.SessionID() == conf.Control.SessionID { + _ = consumer.Stop() + return + } + } } case camera.SessionCommandStart: - if s.consumer == nil { + consumer := s.consumer + if consumer == nil { return } - if !s.consumer.SetConfig(&conf) { + if !consumer.SetConfig(&conf) { log.Warn().Msgf("[homekit] wrong config") return } - if s.streams == nil { - s.streams = map[string]*homekit.Consumer{} - } - - s.streams[conf.Control.SessionID] = s.consumer + s.AddConn(consumer) stream := streams.Get(s.stream) - if err := stream.AddConsumer(s.consumer); err != nil { + if err := stream.AddConsumer(consumer); err != nil { return } go func() { - _, _ = s.consumer.WriteTo(nil) - stream.RemoveConsumer(s.consumer) + _, _ = consumer.WriteTo(nil) + stream.RemoveConsumer(consumer) - delete(s.streams, conf.Control.SessionID) + s.DelConn(consumer) }() } } } func (s *server) GetImage(conn net.Conn, width, height int) []byte { - log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height) + log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height) stream := streams.Get(s.stream) cons := magic.NewKeyframe() @@ -166,69 +351,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte { return b } -func (s *server) GetPair(conn net.Conn, id string) []byte { - log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id) - - for _, pairing := range s.pairings { - if !strings.Contains(pairing, id) { - continue - } - - query, err := url.ParseQuery(pairing) - if err != nil { - continue - } - - if query.Get("client_id") != id { - continue - } - - s := query.Get("client_public") - b, _ := hex.DecodeString(s) - return b - } - return nil -} - -func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) { - log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions) - - query := url.Values{ - "client_id": []string{id}, - "client_public": []string{hex.EncodeToString(public)}, - "permissions": []string{string('0' + permissions)}, - } - if s.GetPair(conn, id) == nil { - s.pairings = append(s.pairings, query.Encode()) - s.UpdateStatus() - s.PatchConfig() - } -} - -func (s *server) DelPair(conn net.Conn, id string) { - log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id) - - id = "client_id=" + id - for i, pairing := range s.pairings { - if !strings.Contains(pairing, id) { - continue - } - - s.pairings = append(s.pairings[:i], s.pairings[i+1:]...) - s.UpdateStatus() - s.PatchConfig() - break - } -} - -func (s *server) PatchConfig() { - if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil { - log.Error().Err(err).Msgf( - "[homekit] can't save %s pairings=%v", s.stream, s.pairings, - ) - } -} - func calcName(name, seed string) string { if name != "" { return name @@ -263,3 +385,21 @@ func calcDevicePrivate(private, seed string) []byte { b := sha512.Sum512([]byte(seed)) return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize]) } + +func calcSetupID(seed string) string { + b := sha512.Sum512([]byte(seed)) + return fmt.Sprintf("%02X%02X", b[44], b[46]) +} + +func calcCategoryID(categoryID string) string { + switch categoryID { + case "bridge": + return hap.CategoryBridge + case "doorbell": + return hap.CategoryDoorbell + } + if core.Atoi(categoryID) > 0 { + return categoryID + } + return hap.CategoryCamera +} diff --git a/internal/mjpeg/init.go b/internal/mjpeg/init.go index 27c557e4..2fa9fa32 100644 --- a/internal/mjpeg/init.go +++ b/internal/mjpeg/init.go @@ -36,7 +36,7 @@ func Init() { var log zerolog.Logger func handlerKeyframe(w http.ResponseWriter, r *http.Request) { - stream := streams.GetOrPatch(r.URL.Query()) + stream, _ := streams.GetOrPatch(r.URL.Query()) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return @@ -145,7 +145,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) { } func handlerWS(tr *ws.Transport, _ *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index cca5220c..d0a6d971 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -91,7 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { return } - stream := streams.GetOrPatch(query) + stream, _ := streams.GetOrPatch(query) if stream == nil { http.Error(w, api.StreamNotFound, http.StatusNotFound) return diff --git a/internal/mp4/ws.go b/internal/mp4/ws.go index c880fb58..c1afac24 100644 --- a/internal/mp4/ws.go +++ b/internal/mp4/ws.go @@ -11,7 +11,7 @@ import ( ) func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } @@ -43,7 +43,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error { } func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error { - stream := streams.GetOrPatch(tr.Request.URL.Query()) + stream, _ := streams.GetOrPatch(tr.Request.URL.Query()) if stream == nil { return errors.New(api.StreamNotFound) } diff --git a/internal/onvif/onvif.go b/internal/onvif/onvif.go index 6dfa633a..3c64cb5c 100644 --- a/internal/onvif/onvif.go +++ b/internal/onvif/onvif.go @@ -45,6 +45,10 @@ func streamOnvif(rawURL string) (core.Producer, error) { log.Debug().Msgf("[onvif] new uri=%s", uri) + if err = streams.Validate(uri); err != nil { + return nil, err + } + return streams.GetProducer(uri) } diff --git a/internal/streams/api.go b/internal/streams/api.go index 28f09708..697d8e67 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -52,8 +52,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { name = src } - if New(name, query["src"]...) == nil { - http.Error(w, "", http.StatusBadRequest) + if _, err := New(name, query["src"]...); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -69,8 +69,8 @@ func apiStreams(w http.ResponseWriter, r *http.Request) { } // support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass - if Patch(name, src) == nil { - http.Error(w, "", http.StatusBadRequest) + if _, err := Patch(name, src); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) } case "POST": @@ -176,3 +176,7 @@ func apiPreload(w http.ResponseWriter, r *http.Request) { http.Error(w, "", http.StatusMethodNotAllowed) } } + +func apiSchemes(w http.ResponseWriter, r *http.Request) { + api.ResponseJSON(w, SupportedSchemes()) +} diff --git a/internal/streams/api_test.go b/internal/streams/api_test.go new file mode 100644 index 00000000..2cb93d2a --- /dev/null +++ b/internal/streams/api_test.go @@ -0,0 +1,66 @@ +package streams + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/stretchr/testify/require" +) + +func TestApiSchemes(t *testing.T) { + // Setup: Register some test handlers and redirects + HandleFunc("rtsp", func(url string) (core.Producer, error) { return nil, nil }) + HandleFunc("rtmp", func(url string) (core.Producer, error) { return nil, nil }) + RedirectFunc("http", func(url string) (string, error) { return "", nil }) + + t.Run("GET request returns schemes", func(t *testing.T) { + req := httptest.NewRequest("GET", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var schemes []string + err := json.Unmarshal(w.Body.Bytes(), &schemes) + require.NoError(t, err) + require.NotEmpty(t, schemes) + + // Check that our test schemes are in the response + require.Contains(t, schemes, "rtsp") + require.Contains(t, schemes, "rtmp") + require.Contains(t, schemes, "http") + }) +} + +func TestApiSchemesNoDuplicates(t *testing.T) { + // Setup: Register a scheme in both handlers and redirects + HandleFunc("duplicate", func(url string) (core.Producer, error) { return nil, nil }) + RedirectFunc("duplicate", func(url string) (string, error) { return "", nil }) + + req := httptest.NewRequest("GET", "/api/schemes", nil) + w := httptest.NewRecorder() + + apiSchemes(w, req) + + require.Equal(t, http.StatusOK, w.Code) + + var schemes []string + err := json.Unmarshal(w.Body.Bytes(), &schemes) + require.NoError(t, err) + + // Count occurrences of "duplicate" + count := 0 + for _, scheme := range schemes { + if scheme == "duplicate" { + count++ + } + } + + // Should only appear once + require.Equal(t, 1, count, "scheme 'duplicate' should appear exactly once") +} diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 3240abb5..9433044b 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -2,6 +2,7 @@ package streams import ( "errors" + "regexp" "strings" "github.com/AlexxIT/go2rtc/pkg/core" @@ -15,6 +16,21 @@ func HandleFunc(scheme string, handler Handler) { handlers[scheme] = handler } +func SupportedSchemes() []string { + uniqueKeys := make(map[string]struct{}, len(handlers)+len(redirects)) + for scheme := range handlers { + uniqueKeys[scheme] = struct{}{} + } + for scheme := range redirects { + uniqueKeys[scheme] = struct{}{} + } + resultKeys := make([]string, 0, len(uniqueKeys)) + for key := range uniqueKeys { + resultKeys = append(resultKeys, key) + } + return resultKeys +} + func HasProducer(url string) bool { if i := strings.IndexByte(url, ':'); i > 0 { scheme := url[:i] @@ -95,3 +111,24 @@ func GetConsumer(url string) (core.Consumer, func(), error) { return nil, nil, errors.New("streams: unsupported scheme: " + url) } + +var insecure = map[string]bool{} + +func MarkInsecure(scheme string) { + insecure[scheme] = true +} + +var sanitize = regexp.MustCompile(`\s`) + +func Validate(source string) error { + // TODO: Review the entire logic of insecure sources + if i := strings.IndexByte(source, ':'); i > 0 { + if insecure[source[:i]] { + return errors.New("streams: source from insecure producer") + } + } + if sanitize.MatchString(source) { + return errors.New("streams: source with spaces may be insecure") + } + return nil +} diff --git a/internal/streams/streams.go b/internal/streams/streams.go index a0b1ed68..433f9d36 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -3,7 +3,6 @@ package streams import ( "errors" "net/url" - "regexp" "sync" "time" @@ -30,6 +29,7 @@ func Init() { api.HandleFunc("api/streams", apiStreams) api.HandleFunc("api/streams.dot", apiStreamsDOT) api.HandleFunc("api/preload", apiPreload) + api.HandleFunc("api/schemes", apiSchemes) if cfg.Publish == nil && cfg.Preload == nil { return @@ -50,20 +50,14 @@ func Init() { }) } -var sanitize = regexp.MustCompile(`\s`) - -// Validate - not allow creating dynamic streams with spaces in the source -func Validate(source string) error { - if sanitize.MatchString(source) { - return errors.New("streams: invalid dynamic source") - } - return nil -} - -func New(name string, sources ...string) *Stream { +func New(name string, sources ...string) (*Stream, error) { for _, source := range sources { - if Validate(source) != nil { - return nil + if !HasProducer(source) { + return nil, errors.New("streams: source not supported") + } + + if err := Validate(source); err != nil { + return nil, err } } @@ -73,10 +67,10 @@ func New(name string, sources ...string) *Stream { streams[name] = stream streamsMu.Unlock() - return stream + return stream, nil } -func Patch(name string, source string) *Stream { +func Patch(name string, source string) (*Stream, error) { streamsMu.Lock() defer streamsMu.Unlock() @@ -88,7 +82,7 @@ func Patch(name string, source string) *Stream { // link (alias) streams[name] to streams[rtspName] streams[name] = stream } - return stream + return stream, nil } } @@ -97,46 +91,44 @@ func Patch(name string, source string) *Stream { // link (alias) streams[name] to streams[source] streams[name] = stream } - return stream + return stream, nil } // check if src has supported scheme if !HasProducer(source) { - return nil + return nil, errors.New("streams: source not supported") } - if Validate(source) != nil { - return nil + if err := Validate(source); err != nil { + return nil, err } // check an existing stream with this name if stream, ok := streams[name]; ok { stream.SetSource(source) - return stream + return stream, nil } // create new stream with this name stream := NewStream(source) streams[name] = stream - return stream + return stream, nil } -func GetOrPatch(query url.Values) *Stream { +func GetOrPatch(query url.Values) (*Stream, error) { // check if src param exists source := query.Get("src") if source == "" { - return nil + return nil, errors.New("streams: source empty") } // check if src is stream name if stream := Get(source); stream != nil { - return stream + return stream, nil } // check if name param provided if name := query.Get("name"); name != "" { - log.Info().Msgf("[streams] create new stream url=%s", source) - return Patch(name, source) } diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index 11e9db89..eca1e12b 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -95,7 +95,7 @@ func asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) { query := tr.Request.URL.Query() if name := query.Get("src"); name != "" { - stream = streams.GetOrPatch(query) + stream, _ = streams.GetOrPatch(query) mode = core.ModePassiveConsumer log.Debug().Str("src", name).Msg("[webrtc] new consumer") } else if name = query.Get("dst"); name != "" { diff --git a/main.go b/main.go index b888eb8d..95e59ddd 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "slices" + "github.com/AlexxIT/go2rtc/internal/alsa" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/api/ws" @@ -35,7 +37,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/srtp" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/internal/tapo" - "github.com/AlexxIT/go2rtc/internal/tuya" "github.com/AlexxIT/go2rtc/internal/v4l2" "github.com/AlexxIT/go2rtc/internal/webrtc" "github.com/AlexxIT/go2rtc/internal/webtorrent" @@ -45,69 +46,67 @@ import ( ) func main() { - app.Version = "1.9.11" + app.Version = "1.9.12" - // 1. Core modules: app, api/ws, streams + type module struct { + name string + init func() + } - app.Init() // init config and logs + modules := []module{ + {"", app.Init}, // init config and logs + {"api", api.Init}, // init API before all others + {"ws", ws.Init}, // init WS API endpoint + {"", streams.Init}, + // Main sources and servers + {"http", http.Init}, // rtsp source, HTTP server + {"rtsp", rtsp.Init}, // rtsp source, RTSP server + {"webrtc", webrtc.Init}, // webrtc source, WebRTC server + // Main API + {"mp4", mp4.Init}, // MP4 API + {"hls", hls.Init}, // HLS API + {"mjpeg", mjpeg.Init}, // MJPEG API + // Other sources and servers + {"hass", hass.Init}, // hass source, Hass API server + {"homekit", homekit.Init}, // homekit source, HomeKit server + {"onvif", onvif.Init}, // onvif source, ONVIF API server + {"rtmp", rtmp.Init}, // rtmp source, RTMP server + {"webtorrent", webtorrent.Init}, // webtorrent source, WebTorrent module + {"wyoming", wyoming.Init}, + // Exec and script sources + {"echo", echo.Init}, + {"exec", exec.Init}, + {"expr", expr.Init}, + {"ffmpeg", ffmpeg.Init}, + // Hardware sources + {"alsa", alsa.Init}, + {"v4l2", v4l2.Init}, + // Other sources + {"bubble", bubble.Init}, + {"doorbird", doorbird.Init}, + {"dvrip", dvrip.Init}, + {"eseecloud", eseecloud.Init}, + {"flussonic", flussonic.Init}, + {"gopro", gopro.Init}, + {"isapi", isapi.Init}, + {"ivideon", ivideon.Init}, + {"mpegts", mpegts.Init}, + {"nest", nest.Init}, + {"ring", ring.Init}, + {"roborock", roborock.Init}, + {"tapo", tapo.Init}, + {"yandex", yandex.Init}, + // Helper modules + {"debug", debug.Init}, + {"ngrok", ngrok.Init}, + {"srtp", srtp.Init}, + } - api.Init() // init API before all others - ws.Init() // init WS API endpoint - - streams.Init() // streams module - - // 2. Main sources and servers - - rtsp.Init() // rtsp source, RTSP server - webrtc.Init() // webrtc source, WebRTC server - - // 3. Main API - - mp4.Init() // MP4 API - hls.Init() // HLS API - mjpeg.Init() // MJPEG API - - // 4. Other sources and servers - - hass.Init() // hass source, Hass API server - onvif.Init() // onvif source, ONVIF API server - webtorrent.Init() // webtorrent source, WebTorrent module - wyoming.Init() - - // 5. Other sources - - rtmp.Init() // rtmp source - exec.Init() // exec source - ffmpeg.Init() // ffmpeg source - echo.Init() // echo source - ivideon.Init() // ivideon source - http.Init() // http/tcp source - dvrip.Init() // dvrip source - tapo.Init() // tapo source - isapi.Init() // isapi source - mpegts.Init() // mpegts passive source - roborock.Init() // roborock source - homekit.Init() // homekit source - ring.Init() // ring source - tuya.Init() // tuya source - nest.Init() // nest source - bubble.Init() // bubble source - expr.Init() // expr source - gopro.Init() // gopro source - doorbird.Init() // doorbird source - v4l2.Init() // v4l2 source - alsa.Init() // alsa source - flussonic.Init() - eseecloud.Init() - yandex.Init() - - // 6. Helper modules - - ngrok.Init() // ngrok module - srtp.Init() // SRTP server - debug.Init() // debug API - - // 7. Go + for _, m := range modules { + if app.Modules == nil || m.name == "" || slices.Contains(app.Modules, m.name) { + m.init() + } + } shell.RunUntilSignal() } diff --git a/pkg/flv/muxer.go b/pkg/flv/muxer.go index 98794265..b04d8981 100644 --- a/pkg/flv/muxer.go +++ b/pkg/flv/muxer.go @@ -34,7 +34,7 @@ func (m *Muxer) GetInit() []byte { switch codec.Name { case core.CodecH264: b[4] |= FlagsVideo - obj["videocodecid"] = CodecAVC + obj["videocodecid"] = CodecH264 case core.CodecAAC: b[4] |= FlagsAudio diff --git a/pkg/flv/producer.go b/pkg/flv/producer.go index 33762d20..9748c94a 100644 --- a/pkg/flv/producer.go +++ b/pkg/flv/producer.go @@ -44,7 +44,9 @@ const ( TagData = 18 CodecAAC = 10 - CodecAVC = 7 + + CodecH264 = 7 + CodecHEVC = 12 ) const ( @@ -207,15 +209,18 @@ func (c *Producer) probe() error { } else { _ = pkt.Payload[0] >> 4 // FrameType - if codecID := pkt.Payload[0] & 0b1111; codecID != CodecAVC { - continue - } - if packetType := pkt.Payload[1]; packetType != PacketTypeAVCHeader { // check if header continue } - codec = h264.ConfigToCodec(pkt.Payload[5:]) + switch codecID := pkt.Payload[0] & 0b1111; codecID { + case CodecH264: + codec = h264.ConfigToCodec(pkt.Payload[5:]) + case CodecHEVC: + codec = h265.ConfigToCodec(pkt.Payload[5:]) + default: + continue + } } media := &core.Media{ diff --git a/pkg/hap/camera/README.md b/pkg/hap/camera/README.md new file mode 100644 index 00000000..c6c6f236 --- /dev/null +++ b/pkg/hap/camera/README.md @@ -0,0 +1,3 @@ +## Useful links + +- https://github.com/bauer-andreas/secure-video-specification diff --git a/pkg/hap/camera/accessory.go b/pkg/hap/camera/accessory.go index 973983ec..37724497 100644 --- a/pkg/hap/camera/accessory.go +++ b/pkg/hap/camera/accessory.go @@ -49,17 +49,17 @@ func ServiceCameraRTPStreamManagement() *hap.Service { val120, _ := tlv8.MarshalBase64(StreamingStatus{ Status: StreamingStatusAvailable, }) - val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + val114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileMain}, Level: []byte{VideoCodecLevel31, VideoCodecLevel40}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, // important for iPhones {Width: 320, Height: 240, Framerate: 15}, // apple watch @@ -67,23 +67,23 @@ func ServiceCameraRTPStreamManagement() *hap.Service { }, }, }) - val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfig{ - Codecs: []AudioCodec{ + val115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfiguration{ + Codecs: []AudioCodecConfiguration{ { CodecType: AudioCodecTypeOpus, - CodecParams: []AudioParams{ + CodecParams: []AudioCodecParameters{ { - Channels: 1, - Bitrate: AudioCodecBitrateVariable, - SampleRate: []byte{AudioCodecSampleRate16Khz}, + Channels: 1, + BitrateMode: AudioCodecBitrateVariable, + SampleRate: []byte{AudioCodecSampleRate16Khz}, }, }, }, }, - ComfortNoise: 0, + ComfortNoiseSupport: 0, }) - val116, _ := tlv8.MarshalBase64(SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, + val116, _ := tlv8.MarshalBase64(SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, }) service := &hap.Service{ diff --git a/pkg/hap/camera/accessory_test.go b/pkg/hap/camera/accessory_test.go index 3f5dcd71..53c99a49 100644 --- a/pkg/hap/camera/accessory_test.go +++ b/pkg/hap/camera/accessory_test.go @@ -63,19 +63,19 @@ func TestAqaraG3(t *testing.T) { { name: "114", value: "AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==", - actual: &SupportedVideoStreamConfig{}, - expect: &SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + actual: &SupportedVideoStreamConfiguration{}, + expect: &SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileMain}, Level: []byte{VideoCodecLevel31, VideoCodecLevel40}, CVOEnabled: []byte{0}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, {Width: 640, Height: 360, Framerate: 30}, @@ -94,29 +94,29 @@ func TestAqaraG3(t *testing.T) { { name: "115", value: "AQ4BAQICCQEBAQIBAAMBAQIBAA==", - actual: &SupportedAudioStreamConfig{}, - expect: &SupportedAudioStreamConfig{ - Codecs: []AudioCodec{ + actual: &SupportedAudioStreamConfiguration{}, + expect: &SupportedAudioStreamConfiguration{ + Codecs: []AudioCodecConfiguration{ { CodecType: AudioCodecTypeAACELD, - CodecParams: []AudioParams{ + CodecParams: []AudioCodecParameters{ { - Channels: 1, - Bitrate: AudioCodecBitrateVariable, - SampleRate: []byte{AudioCodecSampleRate16Khz}, + Channels: 1, + BitrateMode: AudioCodecBitrateVariable, + SampleRate: []byte{AudioCodecSampleRate16Khz}, }, }, }, }, - ComfortNoise: 0, + ComfortNoiseSupport: 0, }, }, { name: "116", value: "AgEAAAACAQEAAAIBAg==", - actual: &SupportedRTPConfig{}, - expect: &SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoNone}, + actual: &SupportedRTPConfiguration{}, + expect: &SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoDisabled}, }, }, } @@ -130,18 +130,18 @@ func TestHomebridge(t *testing.T) { { name: "114", value: "AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAkABAgK0AAMBHgAAAwsBAkABAgLwAAMBDwAAAwsBAkABAgLwAAMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAoAHAgI4BAMBHgAAAwsBAkAGAgKwBAMBHg==", - actual: &SupportedVideoStreamConfig{}, - expect: &SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + actual: &SupportedVideoStreamConfiguration{}, + expect: &SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileConstrainedBaseline, VideoCodecProfileMain, VideoCodecProfileHigh}, Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 320, Height: 180, Framerate: 30}, {Width: 320, Height: 240, Framerate: 15}, @@ -162,9 +162,9 @@ func TestHomebridge(t *testing.T) { { name: "116", value: "AgEA", - actual: &SupportedRTPConfig{}, - expect: &SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, + actual: &SupportedRTPConfiguration{}, + expect: &SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80}, }, }, } @@ -178,18 +178,18 @@ func TestScrypted(t *testing.T) { { name: "114", value: "AVIBAQACEwEBAQIBAAAAAgEBAAACAQIDAQADCwECAA8CAnAIAwEeAAADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECQAECAvAAAwEP", - actual: &SupportedVideoStreamConfig{}, - expect: &SupportedVideoStreamConfig{ - Codecs: []VideoCodec{ + actual: &SupportedVideoStreamConfiguration{}, + expect: &SupportedVideoStreamConfiguration{ + Codecs: []VideoCodecConfiguration{ { CodecType: VideoCodecTypeH264, - CodecParams: []VideoParams{ + CodecParams: []VideoCodecParameters{ { ProfileID: []byte{VideoCodecProfileMain}, Level: []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40}, }, }, - VideoAttrs: []VideoAttrs{ + VideoAttrs: []VideoCodecAttributes{ {Width: 3840, Height: 2160, Framerate: 30}, {Width: 1920, Height: 1080, Framerate: 30}, {Width: 1280, Height: 720, Framerate: 30}, @@ -202,15 +202,15 @@ func TestScrypted(t *testing.T) { { name: "115", value: "AScBAQMCIgEBAQIBAAMBAAAAAwEAAAADAQEAAAMBAQAAAwECAAADAQICAQA=", - actual: &SupportedAudioStreamConfig{}, - expect: &SupportedAudioStreamConfig{ - Codecs: []AudioCodec{ + actual: &SupportedAudioStreamConfiguration{}, + expect: &SupportedAudioStreamConfiguration{ + Codecs: []AudioCodecConfiguration{ { CodecType: AudioCodecTypeOpus, - CodecParams: []AudioParams{ + CodecParams: []AudioCodecParameters{ { - Channels: 1, - Bitrate: AudioCodecBitrateVariable, + Channels: 1, + BitrateMode: AudioCodecBitrateVariable, SampleRate: []byte{ AudioCodecSampleRate8Khz, AudioCodecSampleRate8Khz, AudioCodecSampleRate16Khz, AudioCodecSampleRate16Khz, @@ -220,15 +220,15 @@ func TestScrypted(t *testing.T) { }, }, }, - ComfortNoise: 0, + ComfortNoiseSupport: 0, }, }, { name: "116", value: "AgEAAAACAQI=", - actual: &SupportedRTPConfig{}, - expect: &SupportedRTPConfig{ - CryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoNone}, + actual: &SupportedRTPConfiguration{}, + expect: &SupportedRTPConfiguration{ + SRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoDisabled}, }, }, } diff --git a/pkg/hap/camera/ch114_supported_video.go b/pkg/hap/camera/ch114_supported_video.go index 196f0286..ec70dc61 100644 --- a/pkg/hap/camera/ch114_supported_video.go +++ b/pkg/hap/camera/ch114_supported_video.go @@ -2,15 +2,15 @@ package camera const TypeSupportedVideoStreamConfiguration = "114" -type SupportedVideoStreamConfig struct { - Codecs []VideoCodec `tlv8:"1"` +type SupportedVideoStreamConfiguration struct { + Codecs []VideoCodecConfiguration `tlv8:"1"` } -type VideoCodec struct { - CodecType byte `tlv8:"1"` - CodecParams []VideoParams `tlv8:"2"` - VideoAttrs []VideoAttrs `tlv8:"3"` - RTPParams []RTPParams `tlv8:"4"` +type VideoCodecConfiguration struct { + CodecType byte `tlv8:"1"` + CodecParams []VideoCodecParameters `tlv8:"2"` + VideoAttrs []VideoCodecAttributes `tlv8:"3"` + RTPParams []RTPParams `tlv8:"4"` } //goland:noinspection ALL @@ -31,15 +31,15 @@ const ( VideoCodecCvoSuppported = 1 ) -type VideoParams struct { +type VideoCodecParameters struct { ProfileID []byte `tlv8:"1"` // 0 - baseline, 1 - main, 2 - high Level []byte `tlv8:"2"` // 0 - 3.1, 1 - 3.2, 2 - 4.0 PacketizationMode byte `tlv8:"3"` // only 0 - non interleaved CVOEnabled []byte `tlv8:"4"` // 0 - not supported, 1 - supported - CVOID []byte `tlv8:"5"` // ??? + CVOID []byte `tlv8:"5"` // ID for CVO RTP extensio } -type VideoAttrs struct { +type VideoCodecAttributes struct { Width uint16 `tlv8:"1"` Height uint16 `tlv8:"2"` Framerate uint8 `tlv8:"3"` diff --git a/pkg/hap/camera/ch115_supported_audio.go b/pkg/hap/camera/ch115_supported_audio.go index efb0d881..f7ba9b44 100644 --- a/pkg/hap/camera/ch115_supported_audio.go +++ b/pkg/hap/camera/ch115_supported_audio.go @@ -2,9 +2,9 @@ package camera const TypeSupportedAudioStreamConfiguration = "115" -type SupportedAudioStreamConfig struct { - Codecs []AudioCodec `tlv8:"1"` - ComfortNoise byte `tlv8:"2"` +type SupportedAudioStreamConfiguration struct { + Codecs []AudioCodecConfiguration `tlv8:"1"` + ComfortNoiseSupport byte `tlv8:"2"` } //goland:noinspection ALL @@ -31,16 +31,16 @@ const ( RTPTimeAACLD24 = 40 // 24000/1000*40=960 ) -type AudioCodec struct { - CodecType byte `tlv8:"1"` - CodecParams []AudioParams `tlv8:"2"` - RTPParams []RTPParams `tlv8:"3"` - ComfortNoise []byte `tlv8:"4"` +type AudioCodecConfiguration struct { + CodecType byte `tlv8:"1"` + CodecParams []AudioCodecParameters `tlv8:"2"` + RTPParams []RTPParams `tlv8:"3"` + ComfortNoise []byte `tlv8:"4"` } -type AudioParams struct { - Channels uint8 `tlv8:"1"` - Bitrate byte `tlv8:"2"` // 0 - variable, 1 - constant - SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 - RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60 +type AudioCodecParameters struct { + Channels uint8 `tlv8:"1"` + BitrateMode byte `tlv8:"2"` // 0 - variable, 1 - constant + SampleRate []byte `tlv8:"3"` // 0 - 8000, 1 - 16000, 2 - 24000 + RTPTime []uint8 `tlv8:"4"` // 20, 30, 40, 60 } diff --git a/pkg/hap/camera/ch116_rtp_config.go b/pkg/hap/camera/ch116_supported_rtp.go similarity index 60% rename from pkg/hap/camera/ch116_rtp_config.go rename to pkg/hap/camera/ch116_supported_rtp.go index fb4be550..f0ca0db9 100644 --- a/pkg/hap/camera/ch116_rtp_config.go +++ b/pkg/hap/camera/ch116_supported_rtp.go @@ -6,9 +6,9 @@ const TypeSupportedRTPConfiguration = "116" const ( CryptoAES_CM_128_HMAC_SHA1_80 = 0 CryptoAES_CM_256_HMAC_SHA1_80 = 1 - CryptoNone = 2 + CryptoDisabled = 2 ) -type SupportedRTPConfig struct { - CryptoType []byte `tlv8:"2"` +type SupportedRTPConfiguration struct { + SRTPCryptoType []byte `tlv8:"2"` } diff --git a/pkg/hap/camera/ch117_selected_stream.go b/pkg/hap/camera/ch117_selected_stream.go index aa0c7038..d94ba96b 100644 --- a/pkg/hap/camera/ch117_selected_stream.go +++ b/pkg/hap/camera/ch117_selected_stream.go @@ -2,10 +2,10 @@ package camera const TypeSelectedStreamConfiguration = "117" -type SelectedStreamConfig struct { - Control SessionControl `tlv8:"1"` - VideoCodec VideoCodec `tlv8:"2"` - AudioCodec AudioCodec `tlv8:"3"` +type SelectedStreamConfiguration struct { + Control SessionControl `tlv8:"1"` + VideoCodec VideoCodecConfiguration `tlv8:"2"` + AudioCodec AudioCodecConfiguration `tlv8:"3"` } //goland:noinspection ALL diff --git a/pkg/hap/camera/ch118_setup_endpoints.go b/pkg/hap/camera/ch118_setup_endpoints.go index 9405de4a..e0f426c0 100644 --- a/pkg/hap/camera/ch118_setup_endpoints.go +++ b/pkg/hap/camera/ch118_setup_endpoints.go @@ -2,25 +2,32 @@ package camera const TypeSetupEndpoints = "118" -type SetupEndpoints struct { - SessionID string `tlv8:"1"` - Status []byte `tlv8:"2"` - Address Addr `tlv8:"3"` - VideoCrypto CryptoSuite `tlv8:"4"` - AudioCrypto CryptoSuite `tlv8:"5"` - VideoSSRC []uint32 `tlv8:"6"` - AudioSSRC []uint32 `tlv8:"7"` +type SetupEndpointsRequest struct { + SessionID string `tlv8:"1"` + Address Address `tlv8:"3"` + VideoCrypto SRTPCryptoSuite `tlv8:"4"` + AudioCrypto SRTPCryptoSuite `tlv8:"5"` } -type Addr struct { +type SetupEndpointsResponse struct { + SessionID string `tlv8:"1"` + Status byte `tlv8:"2"` + Address Address `tlv8:"3"` + VideoCrypto SRTPCryptoSuite `tlv8:"4"` + AudioCrypto SRTPCryptoSuite `tlv8:"5"` + VideoSSRC uint32 `tlv8:"6"` + AudioSSRC uint32 `tlv8:"7"` +} + +type Address struct { IPVersion byte `tlv8:"1"` IPAddr string `tlv8:"2"` VideoRTPPort uint16 `tlv8:"3"` AudioRTPPort uint16 `tlv8:"4"` } -type CryptoSuite struct { - CryptoType byte `tlv8:"1"` - MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) - MasterSalt string `tlv8:"3"` // 14 byte +type SRTPCryptoSuite struct { + CryptoSuite byte `tlv8:"1"` + MasterKey string `tlv8:"2"` // 16 (AES_CM_128) or 32 (AES_256_CM) + MasterSalt string `tlv8:"3"` // 14 byte } diff --git a/pkg/hap/camera/ch120_streaming_status.go b/pkg/hap/camera/ch120_streaming_status.go index 2fe53911..e617df27 100644 --- a/pkg/hap/camera/ch120_streaming_status.go +++ b/pkg/hap/camera/ch120_streaming_status.go @@ -9,6 +9,6 @@ type StreamingStatus struct { //goland:noinspection ALL const ( StreamingStatusAvailable = 0 - StreamingStatusBusy = 1 + StreamingStatusInUse = 1 StreamingStatusUnavailable = 2 ) diff --git a/pkg/hap/camera/ch130_data_stream_transport.go b/pkg/hap/camera/ch130_data_stream_transport.go new file mode 100644 index 00000000..808f822d --- /dev/null +++ b/pkg/hap/camera/ch130_data_stream_transport.go @@ -0,0 +1,11 @@ +package camera + +const TypeSupportedDataStreamTransportConfiguration = "130" + +type SupportedDataStreamTransportConfiguration struct { + Configs []TransferTransportConfiguration `tlv8:"1"` +} + +type TransferTransportConfiguration struct { + TransportType byte `tlv8:"1"` +} diff --git a/pkg/hap/camera/ch131_data_stream.go b/pkg/hap/camera/ch131_data_stream.go index 067b01b4..4f4ab49f 100644 --- a/pkg/hap/camera/ch131_data_stream.go +++ b/pkg/hap/camera/ch131_data_stream.go @@ -2,13 +2,13 @@ package camera const TypeSetupDataStreamTransport = "131" -type SetupDataStreamRequest struct { +type SetupDataStreamTransportRequest struct { SessionCommandType byte `tlv8:"1"` TransportType byte `tlv8:"2"` ControllerKeySalt string `tlv8:"3"` } -type SetupDataStreamResponse struct { +type SetupDataStreamTransportResponse struct { Status byte `tlv8:"1"` TransportTypeSessionParameters struct { TCPListeningPort uint16 `tlv8:"1"` diff --git a/pkg/hap/camera/ch205.go b/pkg/hap/camera/ch205.go new file mode 100644 index 00000000..431db7b0 --- /dev/null +++ b/pkg/hap/camera/ch205.go @@ -0,0 +1,18 @@ +package camera + +const TypeSupportedCameraRecordingConfiguration = "205" + +type SupportedCameraRecordingConfiguration struct { + PrebufferLength uint32 `tlv8:"1"` + EventTriggerOptions uint64 `tlv8:"2"` + MediaContainerConfigurations `tlv8:"3"` +} + +type MediaContainerConfigurations struct { + MediaContainerType uint8 `tlv8:"1"` + MediaContainerParameters `tlv8:"2"` +} + +type MediaContainerParameters struct { + FragmentLength uint32 `tlv8:"1"` +} diff --git a/pkg/hap/camera/ch206.go b/pkg/hap/camera/ch206.go new file mode 100644 index 00000000..89219fa7 --- /dev/null +++ b/pkg/hap/camera/ch206.go @@ -0,0 +1,20 @@ +package camera + +const TypeSupportedVideoRecordingConfiguration = "206" + +type SupportedVideoRecordingConfiguration struct { + CodecConfigs []VideoRecordingCodecConfiguration `tlv8:"1"` +} + +type VideoRecordingCodecConfiguration struct { + CodecType uint8 `tlv8:"1"` + CodecParams VideoRecordingCodecParameters `tlv8:"2"` + CodecAttrs VideoCodecAttributes `tlv8:"3"` +} + +type VideoRecordingCodecParameters struct { + ProfileID uint8 `tlv8:"1"` + Level uint8 `tlv8:"2"` + Bitrate uint32 `tlv8:"3"` + IFrameInterval uint32 `tlv8:"4"` +} diff --git a/pkg/hap/camera/ch207.go b/pkg/hap/camera/ch207.go new file mode 100644 index 00000000..5d389923 --- /dev/null +++ b/pkg/hap/camera/ch207.go @@ -0,0 +1,19 @@ +package camera + +const TypeSupportedAudioRecordingConfiguration = "207" + +type SupportedAudioRecordingConfiguration struct { + CodecConfigs []AudioRecordingCodecConfiguration `tlv8:"1"` +} + +type AudioRecordingCodecConfiguration struct { + CodecType byte `tlv8:"1"` + CodecParams []AudioRecordingCodecParameters `tlv8:"2"` +} + +type AudioRecordingCodecParameters struct { + Channels uint8 `tlv8:"1"` + BitrateMode []byte `tlv8:"2"` + SampleRate []byte `tlv8:"3"` + MaxAudioBitrate []uint32 `tlv8:"4"` +} diff --git a/pkg/hap/camera/ch209.go b/pkg/hap/camera/ch209.go new file mode 100644 index 00000000..c51359fb --- /dev/null +++ b/pkg/hap/camera/ch209.go @@ -0,0 +1,9 @@ +package camera + +const TypeSelectedCameraRecordingConfiguration = "209" + +type SelectedCameraRecordingConfiguration struct { + GeneralConfig SupportedCameraRecordingConfiguration `tlv8:"1"` + VideoConfig SupportedVideoRecordingConfiguration `tlv8:"2"` + AudioConfig SupportedAudioRecordingConfiguration `tlv8:"3"` +} diff --git a/pkg/hap/camera/stream.go b/pkg/hap/camera/stream.go index 23d53c39..bda67920 100644 --- a/pkg/hap/camera/stream.go +++ b/pkg/hap/camera/stream.go @@ -15,7 +15,7 @@ type Stream struct { } func NewStream( - client *hap.Client, videoCodec *VideoCodec, audioCodec *AudioCodec, + client *hap.Client, videoCodec *VideoCodecConfiguration, audioCodec *AudioCodecConfiguration, videoSession, audioSession *srtp.Session, bitrate int, ) (*Stream, error) { stream := &Stream{ @@ -58,7 +58,7 @@ func NewStream( } audioCodec.ComfortNoise = []byte{0} - config := &SelectedStreamConfig{ + config := &SelectedStreamConfiguration{ Control: SessionControl{ SessionID: stream.id, Command: SessionCommandStart, @@ -103,19 +103,19 @@ func (s *Stream) GetFreeStream() error { } func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error { - req := SetupEndpoints{ + req := SetupEndpointsRequest{ SessionID: s.id, - Address: Addr{ + Address: Address{ IPVersion: 0, IPAddr: videoSession.Local.Addr, VideoRTPPort: videoSession.Local.Port, AudioRTPPort: audioSession.Local.Port, }, - VideoCrypto: CryptoSuite{ + VideoCrypto: SRTPCryptoSuite{ MasterKey: string(videoSession.Local.MasterKey), MasterSalt: string(videoSession.Local.MasterSalt), }, - AudioCrypto: CryptoSuite{ + AudioCrypto: SRTPCryptoSuite{ MasterKey: string(audioSession.Local.MasterKey), MasterSalt: string(audioSession.Local.MasterSalt), }, @@ -129,7 +129,7 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err return err } - var res SetupEndpoints + var res SetupEndpointsResponse if err := s.client.GetCharacter(char); err != nil { return err } @@ -142,7 +142,7 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err Port: res.Address.VideoRTPPort, MasterKey: []byte(res.VideoCrypto.MasterKey), MasterSalt: []byte(res.VideoCrypto.MasterSalt), - SSRC: res.VideoSSRC[0], + SSRC: res.VideoSSRC, } audioSession.Remote = &srtp.Endpoint{ @@ -150,13 +150,13 @@ func (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) err Port: res.Address.AudioRTPPort, MasterKey: []byte(res.AudioCrypto.MasterKey), MasterSalt: []byte(res.AudioCrypto.MasterSalt), - SSRC: res.AudioSSRC[0], + SSRC: res.AudioSSRC, } return nil } -func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error { +func (s *Stream) SetStreamConfig(config *SelectedStreamConfiguration) error { char := s.service.GetCharacter(TypeSelectedStreamConfiguration) if err := char.Write(config); err != nil { return err @@ -169,7 +169,7 @@ func (s *Stream) SetStreamConfig(config *SelectedStreamConfig) error { } func (s *Stream) Close() error { - config := &SelectedStreamConfig{ + config := &SelectedStreamConfiguration{ Control: SessionControl{ SessionID: s.id, Command: SessionCommandEnd, diff --git a/pkg/hap/client.go b/pkg/hap/client.go index 2c1f7dd3..ed4faa02 100644 --- a/pkg/hap/client.go +++ b/pkg/hap/client.go @@ -18,7 +18,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/hap/curve25519" "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" "github.com/AlexxIT/go2rtc/pkg/mdns" ) @@ -46,7 +45,7 @@ type Client struct { err error } -func NewClient(rawURL string) (*Client, error) { +func Dial(rawURL string) (*Client, error) { u, err := url.Parse(rawURL) if err != nil { return nil, err @@ -61,6 +60,10 @@ func NewClient(rawURL string) (*Client, error) { ClientPrivate: DecodeKey(query.Get("client_private")), } + if err = c.Dial(); err != nil { + return nil, err + } + return c, nil } @@ -96,6 +99,7 @@ func (c *Client) Dial() (err error) { return false }) + // TODO: close conn on error if c.Conn, err = net.DialTimeout("tcp", c.DeviceAddress, ConnDialTimeout); err != nil { return } @@ -124,7 +128,7 @@ func (c *Client) Dial() (err error) { EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(res.Body, &cipherM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM2); err != nil { return err } if cipherM2.State != StateM2 { @@ -209,15 +213,17 @@ func (c *Client) Dial() (err error) { var plainM4 struct { State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil { return } if plainM4.State != StateM4 { return newResponseError(cipherM3, plainM4) } + rw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn)) + // like tls.Client wrapper over net.Conn - if c.Conn, err = secure.Client(c.Conn, sessionShared, true); err != nil { + if c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil { return } // new reader for new conn diff --git a/pkg/hap/client_http.go b/pkg/hap/client_http.go index 360f48bc..7f8314f8 100644 --- a/pkg/hap/client_http.go +++ b/pkg/hap/client_http.go @@ -82,3 +82,20 @@ func ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) { return res, nil } + +func WriteEvent(w io.Writer, res *http.Response) error { + return res.Write(&eventWriter{w: w}) +} + +type eventWriter struct { + w io.Writer + done bool +} + +func (e *eventWriter) Write(p []byte) (n int, err error) { + if !e.done { + p = append([]byte("EVENT/1.0"), p[8:]...) + e.done = true + } + return e.w.Write(p) +} diff --git a/pkg/hap/client_pairing.go b/pkg/hap/client_pairing.go index baec7be5..f253783d 100644 --- a/pkg/hap/client_pairing.go +++ b/pkg/hap/client_pairing.go @@ -107,7 +107,7 @@ func (c *Client) Pair(feature, pin string) (err error) { State byte `tlv8:"6"` Error byte `tlv8:"7"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return } if plainM2.State != StateM2 { @@ -121,9 +121,7 @@ func (c *Client) Pair(feature, pin string) (err error) { username := []byte("Pair-Setup") // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) - pake, err := srp.NewSRP( - "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), - ) + pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username)) if err != nil { return } @@ -132,6 +130,7 @@ func (c *Client) Pair(feature, pin string) (err error) { // username: "Pair-Setup", password: PIN (with dashes) session := pake.NewClientSession(username, []byte(pin)) + sessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey)) if err != nil { return @@ -159,7 +158,7 @@ func (c *Client) Pair(feature, pin string) (err error) { EncryptedData string `tlv8:"5"` // skip EncryptedData validation (for MFi devices) } - if err = tlv8.UnmarshalReader(res.Body, &plainM4); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil { return } if plainM4.State != StateM4 { @@ -232,7 +231,7 @@ func (c *Client) Pair(feature, pin string) (err error) { State byte `tlv8:"6"` Error byte `tlv8:"7"` }{} - if err = tlv8.UnmarshalReader(res.Body, &cipherM6); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM6); err != nil { return } if cipherM6.State != StateM6 || cipherM6.Error != 0 { @@ -296,7 +295,7 @@ func (c *Client) ListPairings() error { State byte `tlv8:"6"` Permission byte `tlv8:"11"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return err } @@ -329,7 +328,7 @@ func (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) e State byte `tlv8:"6"` Unknown byte `tlv8:"7"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return err } @@ -354,7 +353,7 @@ func (c *Client) DeletePairing(id string) error { var plainM2 struct { State byte `tlv8:"6"` } - if err = tlv8.UnmarshalReader(res.Body, &plainM2); err != nil { + if err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil { return err } if plainM2.State != StateM2 { diff --git a/pkg/hap/secure/secure.go b/pkg/hap/conn.go similarity index 66% rename from pkg/hap/secure/secure.go rename to pkg/hap/conn.go index 576ee127..2b039dc8 100644 --- a/pkg/hap/secure/secure.go +++ b/pkg/hap/conn.go @@ -1,32 +1,50 @@ -package secure +package hap import ( "bufio" "encoding/binary" + "encoding/json" "errors" "io" "net" + "sync" "time" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" ) type Conn struct { conn net.Conn - - rd *bufio.Reader - wr *bufio.Writer + rw *bufio.ReadWriter + wmu sync.Mutex encryptKey []byte decryptKey []byte encryptCnt uint64 decryptCnt uint64 + //ClientID string SharedKey []byte + + recv int + send int } -func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { +func (c *Conn) MarshalJSON() ([]byte, error) { + conn := core.Connection{ + ID: core.ID(c), + FormatName: "homekit", + Protocol: "hap", + RemoteAddr: c.conn.RemoteAddr().String(), + Recv: c.recv, + Send: c.send, + } + return json.Marshal(conn) +} + +func NewConn(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) { key1, err := hkdf.Sha512(sharedKey, "Control-Salt", "Control-Read-Encryption-Key") if err != nil { return nil, err @@ -39,8 +57,7 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { c := &Conn{ conn: conn, - rd: bufio.NewReaderSize(conn, 32*1024), - wr: bufio.NewWriterSize(conn, 32*1024), + rw: rw, SharedKey: sharedKey, } @@ -55,8 +72,8 @@ func Client(conn net.Conn, sharedKey []byte, isClient bool) (net.Conn, error) { } const ( - // PacketSizeMax is the max length of encrypted packets - PacketSizeMax = 0x400 + // packetSizeMax is the max length of encrypted packets + packetSizeMax = 0x400 VerifySize = 2 NonceSize = 8 @@ -64,19 +81,19 @@ const ( ) func (c *Conn) Read(b []byte) (n int, err error) { - if cap(b) < PacketSizeMax { + if cap(b) < packetSizeMax { return 0, errors.New("hap: read buffer is too small") } - verify := make([]byte, 2) // verify = plain message size - if _, err = io.ReadFull(c.rd, verify); err != nil { + verify := make([]byte, VerifySize) // verify = plain message size + if _, err = io.ReadFull(c.rw, verify); err != nil { return } n = int(binary.LittleEndian.Uint16(verify)) - ciphertext := make([]byte, n+Overhead) - if _, err = io.ReadFull(c.rd, ciphertext); err != nil { + ciphertext := make([]byte, n+Overhead) + if _, err = io.ReadFull(c.rw, ciphertext); err != nil { return } @@ -85,22 +102,27 @@ func (c *Conn) Read(b []byte) (n int, err error) { c.decryptCnt++ _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify) + + c.recv += n return } func (c *Conn) Write(b []byte) (n int, err error) { - buf := make([]byte, 0, PacketSizeMax+Overhead) + c.wmu.Lock() + defer c.wmu.Unlock() + + buf := make([]byte, 0, packetSizeMax+Overhead) nonce := make([]byte, NonceSize) verify := make([]byte, VerifySize) for len(b) > 0 { size := len(b) - if size > PacketSizeMax { - size = PacketSizeMax + if size > packetSizeMax { + size = packetSizeMax } binary.LittleEndian.PutUint16(verify, uint16(size)) - if _, err = c.wr.Write(verify); err != nil { + if _, err = c.rw.Write(verify); err != nil { return } @@ -112,7 +134,7 @@ func (c *Conn) Write(b []byte) (n int, err error) { return } - if _, err = c.wr.Write(buf[:size+Overhead]); err != nil { + if _, err = c.rw.Write(buf[:size+Overhead]); err != nil { return } @@ -120,7 +142,9 @@ func (c *Conn) Write(b []byte) (n int, err error) { n += size } - err = c.wr.Flush() + err = c.rw.Flush() + + c.send += n return } diff --git a/pkg/hap/hds/hds.go b/pkg/hap/hds/hds.go index a7b2c74a..0e299919 100644 --- a/pkg/hap/hds/hds.go +++ b/pkg/hap/hds/hds.go @@ -4,16 +4,19 @@ package hds import ( "bufio" "encoding/binary" + "encoding/json" + "errors" "io" "net" "time" + "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" ) -func Client(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { +func NewConn(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) { writeKey, err := hkdf.Sha512(key, salt, "HDS-Write-Encryption-Key") if err != nil { return nil, err @@ -49,43 +52,91 @@ type Conn struct { encryptKey []byte decryptCnt uint64 encryptCnt uint64 + + recv int + send int } -func (c *Conn) Read(p []byte) (n int, err error) { +func (c *Conn) MarshalJSON() ([]byte, error) { + conn := core.Connection{ + ID: core.ID(c), + FormatName: "homekit", + Protocol: "hds", + RemoteAddr: c.conn.RemoteAddr().String(), + Recv: c.recv, + Send: c.send, + } + return json.Marshal(conn) +} + +func (c *Conn) read() (b []byte, err error) { verify := make([]byte, 4) if _, err = io.ReadFull(c.rd, verify); err != nil { return } - n = int(binary.BigEndian.Uint32(verify) & 0xFFFFFF) + n := int(binary.BigEndian.Uint32(verify) & 0xFFFFFF) - ciphertext := make([]byte, n+secure.Overhead) + ciphertext := make([]byte, n+hap.Overhead) if _, err = io.ReadFull(c.rd, ciphertext); err != nil { return } - nonce := make([]byte, secure.NonceSize) + nonce := make([]byte, hap.NonceSize) binary.LittleEndian.PutUint64(nonce, c.decryptCnt) c.decryptCnt++ - _, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, p[:0], nonce, ciphertext, verify) + c.recv += n + + return chacha20poly1305.DecryptAndVerify(c.decryptKey, ciphertext[:0], nonce, ciphertext, verify) +} + +func (c *Conn) Read(p []byte) (n int, err error) { + b, err := c.read() + if err != nil { + return 0, err + } + n = copy(p, b) + if len(b) > n { + err = errors.New("hds: read buffer too small") + } return } +func (c *Conn) WriteTo(w io.Writer) (int64, error) { + var total int64 + for { + b, err := c.read() + if err != nil { + return total, err + } + + n, err := w.Write(b) + total += int64(n) + if err != nil { + return total, err + } + } +} + func (c *Conn) Write(b []byte) (n int, err error) { n = len(b) + if n > 0xFFFFFF { + return 0, errors.New("hds: write buffer too big") + } + verify := make([]byte, 4) binary.BigEndian.PutUint32(verify, 0x01000000|uint32(n)) if _, err = c.wr.Write(verify); err != nil { return } - nonce := make([]byte, secure.NonceSize) + nonce := make([]byte, hap.NonceSize) binary.LittleEndian.PutUint64(nonce, c.encryptCnt) c.encryptCnt++ - buf := make([]byte, n+secure.Overhead) + buf := make([]byte, n+hap.Overhead) if _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil { return } @@ -95,6 +146,8 @@ func (c *Conn) Write(b []byte) (n int, err error) { } err = c.wr.Flush() + + c.send += n return } diff --git a/pkg/hap/helpers.go b/pkg/hap/helpers.go index 3900f935..3c3b287c 100644 --- a/pkg/hap/helpers.go +++ b/pkg/hap/helpers.go @@ -3,6 +3,8 @@ package hap import ( "crypto/ed25519" "crypto/rand" + "crypto/sha512" + "encoding/base64" "encoding/hex" "errors" "fmt" @@ -99,6 +101,12 @@ func GenerateUUID() string { return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:] } +func SetupHash(setupID, deviceID string) string { + // should be setup_id (random 4 alphanum) + device_id (mac address) + b := sha512.Sum512([]byte(setupID + deviceID)) + return base64.StdEncoding.EncodeToString(b[:4]) +} + func Append(items ...any) (b []byte) { for _, item := range items { switch v := item.(type) { diff --git a/pkg/hap/server.go b/pkg/hap/server.go index 2a912324..a992528c 100644 --- a/pkg/hap/server.go +++ b/pkg/hap/server.go @@ -3,32 +3,25 @@ package hap import ( "bufio" "crypto/sha512" - "encoding/base64" "errors" "fmt" - "io" - "net" "net/http" "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" "github.com/AlexxIT/go2rtc/pkg/hap/curve25519" "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" + "github.com/tadglines/go-pkgs/crypto/srp" ) -type HandlerFunc func(net.Conn) error - type Server struct { Pin string DeviceID string DevicePrivate []byte - GetPair func(conn net.Conn, id string) []byte - AddPair func(conn net.Conn, id string, public []byte, permissions byte) - - Handler HandlerFunc + // GetClientPublic may be nil, so client validation will be disabled + GetClientPublic func(id string) []byte } func (s *Server) ServerPublic() []byte { @@ -42,44 +35,240 @@ func (s *Server) ServerPublic() []byte { // return StatusPaired //} -func (s *Server) SetupHash() string { - // should be setup_id (random 4 alphanum) + device_id (mac address) - // but device_id is random, so OK - b := sha512.Sum512([]byte(s.DeviceID)) - return base64.StdEncoding.EncodeToString(b[:4]) -} - -func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - // Request from iPhone +func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) { + // STEP 1. Request from iPhone var plainM1 struct { - PublicKey string `tlv8:"3"` - State byte `tlv8:"6"` + State byte `tlv8:"6"` + Method byte `tlv8:"0"` + Flags uint32 `tlv8:"19"` } - if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil { - return err + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { + return } if plainM1.State != StateM1 { - return newRequestError(plainM1) + err = newRequestError(plainM1) + return + } + + username := []byte("Pair-Setup") + + // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) + pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username)) + if err != nil { + return + } + + pake.SaltLength = 16 + + salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin)) + if err != nil { + return + } + + session := pake.NewServerSession(username, salt, verifier) + + // STEP 2. Response to iPhone + plainM2 := struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + Salt string `tlv8:"2"` + }{ + State: StateM2, + PublicKey: string(session.GetB()), + Salt: string(salt), + } + body, err := tlv8.Marshal(plainM2) + if err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + // STEP 3. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return + } + + var plainM3 struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + Proof string `tlv8:"4"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil { + return + } + if plainM3.State != StateM3 { + err = newRequestError(plainM3) + return + } + + // important to compute key before verify client + sessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey)) + if err != nil { + return + } + + if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) { + err = errors.New("hap: VerifyClientAuthenticator") + return + } + + proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof + + // STEP 4. Response to iPhone + payloadM4 := struct { + State byte `tlv8:"6"` + Proof string `tlv8:"4"` + }{ + State: StateM4, + Proof: string(proof), + } + if body, err = tlv8.Marshal(payloadM4); err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + // STEP 5. Request from iPhone + if req, err = http.ReadRequest(rw.Reader); err != nil { + return + } + var cipherM5 struct { + State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil { + return + } + if cipherM5.State != StateM5 { + err = newRequestError(cipherM5) + return + } + + // decrypt message using session shared + encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") + if err != nil { + return + } + + b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData)) + if err != nil { + return + } + + // unpack message from TLV8 + var plainM5 struct { + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` + } + if err = tlv8.Unmarshal(b, &plainM5); err != nil { + return + } + + // 3. verify client ID and Public + remoteSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", + ) + if err != nil { + return + } + + b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey) + if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) { + err = errors.New("hap: ValidateSignature") + return + } + + // 4. generate signature to our ID and Public + localSign, err := hkdf.Sha512( + sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", + ) + if err != nil { + return + } + + b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic + signature, err := ed25519.Signature(s.DevicePrivate, b) + if err != nil { + return + } + + // 5. pack our ID and Public + plainM6 := struct { + Identifier string `tlv8:"1"` + PublicKey string `tlv8:"3"` + Signature string `tlv8:"10"` + }{ + Identifier: s.DeviceID, + PublicKey: string(s.ServerPublic()), + Signature: string(signature), + } + if b, err = tlv8.Marshal(plainM6); err != nil { + return + } + + // 6. encrypt message + b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b) + if err != nil { + return + } + + // STEP 6. Response to iPhone + cipherM6 := struct { + State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` + }{ + State: StateM6, + EncryptedData: string(b), + } + if body, err = tlv8.Marshal(cipherM6); err != nil { + return + } + if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { + return + } + + id = plainM5.Identifier + publicKey = []byte(plainM5.PublicKey) + + return +} + +func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) { + // Request from iPhone + var plainM1 struct { + State byte `tlv8:"6"` + PublicKey string `tlv8:"3"` + } + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil { + return + } + if plainM1.State != StateM1 { + err = newRequestError(plainM1) + return } // Generate the key pair sessionPublic, sessionPrivate := curve25519.GenerateKeyPair() sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey)) if err != nil { - return err + return } encryptKey, err := hkdf.Sha512( sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info", ) if err != nil { - return err + return } b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey) signature, err := ed25519.Signature(s.DevicePrivate, b) if err != nil { - return err + return } // STEP M2. Response to iPhone @@ -91,12 +280,12 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co Signature: string(signature), } if b, err = tlv8.Marshal(plainM2); err != nil { - return err + return } b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b) if err != nil { - return err + return } cipherM2 := struct { @@ -110,30 +299,32 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co } body, err := tlv8.Marshal(cipherM2) if err != nil { - return err + return } if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err + return } // STEP M3. Request from iPhone if req, err = http.ReadRequest(rw.Reader); err != nil { - return err + return } var cipherM3 struct { - EncryptedData string `tlv8:"5"` State byte `tlv8:"6"` + EncryptedData string `tlv8:"5"` } - if err = tlv8.UnmarshalReader(req.Body, &cipherM3); err != nil { - return err + if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil { + return } if cipherM3.State != StateM3 { - return newRequestError(cipherM3) + err = newRequestError(cipherM3) + return } - if b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)); err != nil { - return err + b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData)) + if err != nil { + return } var plainM3 struct { @@ -141,17 +332,21 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co Signature string `tlv8:"10"` } if err = tlv8.Unmarshal(b, &plainM3); err != nil { - return err + return } - clientPublic := s.GetPair(conn, plainM3.Identifier) - if clientPublic == nil { - return fmt.Errorf("hap: PairVerify from: %s, with unknown client_id: %s", conn.RemoteAddr(), plainM3.Identifier) - } + if s.GetClientPublic != nil { + clientPublic := s.GetClientPublic(plainM3.Identifier) + if clientPublic == nil { + err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier) + return + } - b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) - if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { - return errors.New("new: ValidateSignature") + b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic) + if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) { + err = errors.New("hap: ValidateSignature") + return + } } // STEP M4. Response to iPhone @@ -161,15 +356,41 @@ func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter, conn net.Co State: StateM4, } if body, err = tlv8.Marshal(payloadM4); err != nil { - return err + return } if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err + return } - if conn, err = secure.Client(conn, sessionShared, false); err != nil { - return err - } + id = plainM3.Identifier + sessionKey = sessionShared - return s.Handler(conn) + return } + +func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error { + header := fmt.Sprintf( + "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", + statusCode, http.StatusText(statusCode), contentType, len(body), + ) + body = append([]byte(header), body...) + if _, err := w.Write(body); err != nil { + return err + } + return w.Flush() +} + +//func WriteBackoff(rw *bufio.ReadWriter) error { +// plainM2 := struct { +// State byte `tlv8:"6"` +// Error byte `tlv8:"7"` +// }{ +// State: StateM2, +// Error: 3, // BackoffError +// } +// body, err := tlv8.Marshal(plainM2) +// if err != nil { +// return err +// } +// return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) +//} diff --git a/pkg/hap/server_pairing.go b/pkg/hap/server_pairing.go deleted file mode 100644 index 77895c10..00000000 --- a/pkg/hap/server_pairing.go +++ /dev/null @@ -1,252 +0,0 @@ -package hap - -import ( - "bufio" - "crypto/sha512" - "errors" - "fmt" - "io" - "net" - "net/http" - - "github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305" - "github.com/AlexxIT/go2rtc/pkg/hap/ed25519" - "github.com/AlexxIT/go2rtc/pkg/hap/hkdf" - "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" - "github.com/tadglines/go-pkgs/crypto/srp" -) - -const ( - PairMethodSetup = iota - PairMethodSetupWithAuth - PairMethodVerify - PairMethodAdd - PairMethodRemove - PairMethodList -) - -func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter, conn net.Conn) error { - if req.Header.Get("Content-Type") != MimeTLV8 { - return errors.New("hap: wrong content type") - } - - // STEP 1. Request from iPhone - var plainM1 struct { - Method byte `tlv8:"0"` - State byte `tlv8:"6"` - Flags uint32 `tlv8:"19"` - } - if err := tlv8.UnmarshalReader(io.LimitReader(rw, req.ContentLength), &plainM1); err != nil { - return err - } - if plainM1.State != StateM1 { - return newRequestError(plainM1) - } - - username := []byte("Pair-Setup") - - // Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE) - pake, err := srp.NewSRP( - "rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username), - ) - if err != nil { - return err - } - - pake.SaltLength = 16 - - salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin)) - - session := pake.NewServerSession(username, salt, verifier) - - // STEP 2. Response to iPhone - plainM2 := struct { - Salt string `tlv8:"2"` - PublicKey string `tlv8:"3"` - State byte `tlv8:"6"` - }{ - State: StateM2, - PublicKey: string(session.GetB()), - Salt: string(salt), - } - body, err := tlv8.Marshal(plainM2) - if err != nil { - return err - } - if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err - } - - // STEP 3. Request from iPhone - if req, err = http.ReadRequest(rw.Reader); err != nil { - return err - } - - var plainM3 struct { - SessionKey string `tlv8:"3"` - Proof string `tlv8:"4"` - State byte `tlv8:"6"` - } - if err = tlv8.UnmarshalReader(req.Body, &plainM3); err != nil { - return err - } - if plainM3.State != StateM3 { - return newRequestError(plainM3) - } - - // important to compute key before verify client - sessionShared, err := session.ComputeKey([]byte(plainM3.SessionKey)) - if err != nil { - return err - } - - if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) { - return errors.New("hap: VerifyClientAuthenticator") - } - - proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof - - // STEP 4. Response to iPhone - payloadM4 := struct { - Proof string `tlv8:"4"` - State byte `tlv8:"6"` - }{ - Proof: string(proof), - State: StateM4, - } - if body, err = tlv8.Marshal(payloadM4); err != nil { - return err - } - if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err - } - - // STEP 5. Request from iPhone - if req, err = http.ReadRequest(rw.Reader); err != nil { - return err - } - var cipherM5 struct { - EncryptedData string `tlv8:"5"` - State byte `tlv8:"6"` - } - if err = tlv8.UnmarshalReader(req.Body, &cipherM5); err != nil { - return err - } - if cipherM5.State != StateM5 { - return newRequestError(cipherM5) - } - - // decrypt message using session shared - encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info") - if err != nil { - return err - } - - b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData)) - if err != nil { - return err - } - - // unpack message from TLV8 - var plainM5 struct { - Identifier string `tlv8:"1"` - PublicKey string `tlv8:"3"` - Signature string `tlv8:"10"` - } - if err = tlv8.Unmarshal(b, &plainM5); err != nil { - return err - } - - // 3. verify client ID and Public - remoteSign, err := hkdf.Sha512( - sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info", - ) - if err != nil { - return err - } - - b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey) - if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) { - return errors.New("hap: ValidateSignature") - } - - // 4. generate signature to our ID and Public - localSign, err := hkdf.Sha512( - sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info", - ) - if err != nil { - return err - } - - b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic - signature, err := ed25519.Signature(s.DevicePrivate, b) - if err != nil { - return err - } - - // 5. pack our ID and Public - plainM6 := struct { - Identifier string `tlv8:"1"` - PublicKey string `tlv8:"3"` - Signature string `tlv8:"10"` - }{ - Identifier: s.DeviceID, - PublicKey: string(s.ServerPublic()), - Signature: string(signature), - } - if b, err = tlv8.Marshal(plainM6); err != nil { - return err - } - - // 6. encrypt message - b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b) - if err != nil { - return err - } - - // STEP 6. Response to iPhone - cipherM6 := struct { - EncryptedData string `tlv8:"5"` - State byte `tlv8:"6"` - }{ - State: StateM6, - EncryptedData: string(b), - } - if body, err = tlv8.Marshal(cipherM6); err != nil { - return err - } - if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil { - return err - } - - s.AddPair(conn, plainM5.Identifier, []byte(plainM5.PublicKey), PermissionAdmin) - - return nil -} - -func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error { - header := fmt.Sprintf( - "HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n", - statusCode, http.StatusText(statusCode), contentType, len(body), - ) - body = append([]byte(header), body...) - if _, err := w.Write(body); err != nil { - return err - } - return w.Flush() -} - -func WriteBackoff(rw *bufio.ReadWriter) error { - plainM2 := struct { - State byte `tlv8:"6"` - Error byte `tlv8:"7"` - }{ - State: StateM2, - Error: 3, // BackoffError - } - body, err := tlv8.Marshal(plainM2) - if err != nil { - return err - } - return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body) -} diff --git a/pkg/hap/setup/setup.go b/pkg/hap/setup/setup.go new file mode 100644 index 00000000..c5eeb51b --- /dev/null +++ b/pkg/hap/setup/setup.go @@ -0,0 +1,32 @@ +package setup + +import ( + "strconv" + "strings" +) + +const ( + FlagNFC = 1 + FlagIP = 2 + FlagBLE = 4 + FlagWAC = 8 // Wireless Accessory Configuration (WAC)/Apples MFi +) + +func GenerateSetupURI(category, pin, setupID string) string { + c, _ := strconv.Atoi(category) + p, _ := strconv.Atoi(strings.ReplaceAll(pin, "-", "")) + payload := int64(c&0xFF)<<31 | int64(FlagIP&0xF)<<27 | int64(p&0x7FFFFFF) + return "X-HM://" + FormatInt36(payload, 9) + setupID +} + +// FormatInt36 equal to strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36))) +func FormatInt36(value int64, n int) string { + b := make([]byte, n) + for i := n - 1; 0 <= i; i-- { + b[i] = digits[value%36] + value /= 36 + } + return string(b) +} + +const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" diff --git a/pkg/hap/setup/setup_test.go b/pkg/hap/setup/setup_test.go new file mode 100644 index 00000000..01672218 --- /dev/null +++ b/pkg/hap/setup/setup_test.go @@ -0,0 +1,18 @@ +package setup + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFormatAlphaNum(t *testing.T) { + value := int64(999) + n := 5 + s1 := strings.ToUpper(fmt.Sprintf("%0"+strconv.Itoa(n)+"s", strconv.FormatInt(value, 36))) + s2 := FormatInt36(value, n) + require.Equal(t, s1, s2) +} diff --git a/pkg/hap/tlv8/tlv8.go b/pkg/hap/tlv8/tlv8.go index 7af27ea4..7b397b99 100644 --- a/pkg/hap/tlv8/tlv8.go +++ b/pkg/hap/tlv8/tlv8.go @@ -112,6 +112,10 @@ func appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) { v := value.Uint() return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil + case reflect.Uint64: + v := value.Uint() + return binary.LittleEndian.AppendUint64(append(b, tag, 8), v), nil + case reflect.Float32: v := math.Float32bits(float32(value.Float())) return append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil @@ -170,11 +174,20 @@ func UnmarshalBase64(in any, out any) error { return Unmarshal(data, out) } -func UnmarshalReader(r io.Reader, v any) error { - data, err := io.ReadAll(r) +func UnmarshalReader(r io.Reader, n int64, v any) error { + var data []byte + var err error + + if n > 0 { + data = make([]byte, n) + _, err = io.ReadFull(r, data) + } else { + data, err = io.ReadAll(r) + } if err != nil { return err } + return Unmarshal(data, v) } @@ -301,6 +314,12 @@ func unmarshalValue(v []byte, value reflect.Value) error { } value.SetUint(uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24) + case reflect.Uint64: + if len(v) != 8 { + return errors.New("tlv8: wrong size: " + value.Type().Name()) + } + value.SetUint(binary.LittleEndian.Uint64(v)) + case reflect.Float32: f := math.Float32frombits(binary.LittleEndian.Uint32(v)) value.SetFloat(float64(f)) diff --git a/pkg/homekit/consumer.go b/pkg/homekit/consumer.go index ea83146f..c1be7447 100644 --- a/pkg/homekit/consumer.go +++ b/pkg/homekit/consumer.go @@ -49,7 +49,7 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { Connection: core.Connection{ ID: core.NewID(), FormatName: "homekit", - Protocol: "udp", + Protocol: "rtp", RemoteAddr: conn.RemoteAddr().String(), Medias: medias, Transport: conn, @@ -59,7 +59,11 @@ func NewConsumer(conn net.Conn, server *srtp.Server) *Consumer { } } -func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) { +func (c *Consumer) SessionID() string { + return c.sessionID +} + +func (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) { c.sessionID = offer.SessionID c.videoSession = &srtp.Session{ Remote: &srtp.Endpoint{ @@ -79,32 +83,32 @@ func (c *Consumer) SetOffer(offer *camera.SetupEndpoints) { } } -func (c *Consumer) GetAnswer() *camera.SetupEndpoints { +func (c *Consumer) GetAnswer() *camera.SetupEndpointsResponse { c.videoSession.Local = c.srtpEndpoint() c.audioSession.Local = c.srtpEndpoint() - return &camera.SetupEndpoints{ + return &camera.SetupEndpointsResponse{ SessionID: c.sessionID, - Status: []byte{0}, - Address: camera.Addr{ + Status: camera.StreamingStatusAvailable, + Address: camera.Address{ IPAddr: c.videoSession.Local.Addr, VideoRTPPort: c.videoSession.Local.Port, AudioRTPPort: c.audioSession.Local.Port, }, - VideoCrypto: camera.CryptoSuite{ + VideoCrypto: camera.SRTPCryptoSuite{ MasterKey: string(c.videoSession.Local.MasterKey), MasterSalt: string(c.videoSession.Local.MasterSalt), }, - AudioCrypto: camera.CryptoSuite{ + AudioCrypto: camera.SRTPCryptoSuite{ MasterKey: string(c.audioSession.Local.MasterKey), MasterSalt: string(c.audioSession.Local.MasterSalt), }, - VideoSSRC: []uint32{c.videoSession.Local.SSRC}, - AudioSSRC: []uint32{c.audioSession.Local.SSRC}, + VideoSSRC: c.videoSession.Local.SSRC, + AudioSSRC: c.audioSession.Local.SSRC, } } -func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfig) bool { +func (c *Consumer) SetConfig(conf *camera.SelectedStreamConfiguration) bool { if c.sessionID != conf.Control.SessionID { return false } diff --git a/pkg/homekit/helpers.go b/pkg/homekit/helpers.go index f5a17319..625e3ab7 100644 --- a/pkg/homekit/helpers.go +++ b/pkg/homekit/helpers.go @@ -13,7 +13,7 @@ var videoCodecs = [...]string{core.CodecH264} var videoProfiles = [...]string{"4200", "4D00", "6400"} var videoLevels = [...]string{"1F", "20", "28"} -func videoToMedia(codecs []camera.VideoCodec) *core.Media { +func videoToMedia(codecs []camera.VideoCodecConfiguration) *core.Media { media := &core.Media{ Kind: core.KindVideo, Direction: core.DirectionRecvonly, } @@ -39,7 +39,7 @@ func videoToMedia(codecs []camera.VideoCodec) *core.Media { var audioCodecs = [...]string{core.CodecPCMU, core.CodecPCMA, core.CodecELD, core.CodecOpus} var audioSampleRates = [...]uint32{8000, 16000, 24000} -func audioToMedia(codecs []camera.AudioCodec) *core.Media { +func audioToMedia(codecs []camera.AudioCodecConfiguration) *core.Media { media := &core.Media{ Kind: core.KindAudio, Direction: core.DirectionRecvonly, } @@ -67,10 +67,10 @@ func audioToMedia(codecs []camera.AudioCodec) *core.Media { return media } -func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.VideoCodec { +func trackToVideo(track *core.Receiver, video0 *camera.VideoCodecConfiguration, maxWidth, maxHeight int) *camera.VideoCodecConfiguration { profileID := video0.CodecParams[0].ProfileID[0] level := video0.CodecParams[0].Level[0] - attrs := video0.VideoAttrs[0] + var attrs camera.VideoCodecAttributes if track != nil { profile := h264.GetProfileLevelID(track.Codec.FmtpLine) @@ -90,25 +90,28 @@ func trackToVideo(track *core.Receiver, video0 *camera.VideoCodec) *camera.Video } for _, s := range video0.VideoAttrs { + if (maxWidth > 0 && int(s.Width) > maxWidth) || (maxHeight > 0 && int(s.Height) > maxHeight) { + continue + } if s.Width > attrs.Width || s.Height > attrs.Height { attrs = s } } } - return &camera.VideoCodec{ + return &camera.VideoCodecConfiguration{ CodecType: video0.CodecType, - CodecParams: []camera.VideoParams{ + CodecParams: []camera.VideoCodecParameters{ { ProfileID: []byte{profileID}, Level: []byte{level}, }, }, - VideoAttrs: []camera.VideoAttrs{attrs}, + VideoAttrs: []camera.VideoCodecAttributes{attrs}, } } -func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.AudioCodec { +func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodecConfiguration) *camera.AudioCodecConfiguration { codecType := audio0.CodecType channels := audio0.CodecParams[0].Channels sampleRate := audio0.CodecParams[0].SampleRate[0] @@ -131,9 +134,9 @@ func trackToAudio(track *core.Receiver, audio0 *camera.AudioCodec) *camera.Audio } } - return &camera.AudioCodec{ + return &camera.AudioCodecConfiguration{ CodecType: codecType, - CodecParams: []camera.AudioParams{ + CodecParams: []camera.AudioCodecParameters{ { Channels: channels, SampleRate: []byte{sampleRate}, diff --git a/pkg/homekit/log/debug.go b/pkg/homekit/log/debug.go new file mode 100644 index 00000000..1fb60be2 --- /dev/null +++ b/pkg/homekit/log/debug.go @@ -0,0 +1,45 @@ +package log + +import ( + "bytes" + "io" + "log" + "net/http" +) + +func Debug(v any) { + switch v := v.(type) { + case *http.Request: + if v == nil { + return + } + if v.ContentLength != 0 { + b, err := io.ReadAll(v.Body) + if err != nil { + panic(err) + } + v.Body = io.NopCloser(bytes.NewReader(b)) + log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b) + } else { + log.Printf("[homekit] request: %s %s ", v.Method, v.RequestURI) + } + case *http.Response: + if v == nil { + return + } + if v.Header.Get("Content-Type") == "image/jpeg" { + log.Printf("[homekit] response: %d ", v.StatusCode) + return + } + if v.ContentLength != 0 { + b, err := io.ReadAll(v.Body) + if err != nil { + panic(err) + } + v.Body = io.NopCloser(bytes.NewReader(b)) + log.Printf("[homekit] response: %s %d\n%s", v.Proto, v.StatusCode, b) + } else { + log.Printf("[homekit] response: %s %d ", v.Proto, v.StatusCode) + } + } +} diff --git a/pkg/homekit/producer.go b/pkg/homekit/producer.go index 451b9882..81352a13 100644 --- a/pkg/homekit/producer.go +++ b/pkg/homekit/producer.go @@ -5,7 +5,6 @@ import ( "fmt" "math/rand" "net" - "net/url" "time" "github.com/AlexxIT/go2rtc/pkg/core" @@ -22,36 +21,25 @@ type Client struct { hap *hap.Client srtp *srtp.Server - videoConfig camera.SupportedVideoStreamConfig - audioConfig camera.SupportedAudioStreamConfig + videoConfig camera.SupportedVideoStreamConfiguration + audioConfig camera.SupportedAudioStreamConfiguration videoSession *srtp.Session audioSession *srtp.Session stream *camera.Stream - Bitrate int // in bits/s + MaxWidth int `json:"-"` + MaxHeight int `json:"-"` + Bitrate int `json:"-"` // in bits/s } func Dial(rawURL string, server *srtp.Server) (*Client, error) { - u, err := url.Parse(rawURL) + conn, err := hap.Dial(rawURL) if err != nil { return nil, err } - query := u.Query() - conn := &hap.Client{ - DeviceAddress: u.Host, - DeviceID: query.Get("device_id"), - DevicePublic: hap.DecodeKey(query.Get("device_public")), - ClientID: query.Get("client_id"), - ClientPrivate: hap.DecodeKey(query.Get("client_private")), - } - - if err = conn.Dial(); err != nil { - return nil, err - } - client := &Client{ Connection: core.Connection{ ID: core.NewID(), @@ -129,7 +117,7 @@ func (c *Client) Start() error { } videoTrack := c.trackByKind(core.KindVideo) - videoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0]) + videoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0], c.MaxWidth, c.MaxHeight) audioTrack := c.trackByKind(core.KindAudio) audioCodec := trackToAudio(audioTrack, &c.audioConfig.Codecs[0]) diff --git a/pkg/homekit/proxy.go b/pkg/homekit/proxy.go index be233042..2132266c 100644 --- a/pkg/homekit/proxy.go +++ b/pkg/homekit/proxy.go @@ -4,31 +4,30 @@ import ( "bufio" "bytes" "encoding/json" - "fmt" "io" "net" "net/http" + "time" "github.com/AlexxIT/go2rtc/pkg/hap" "github.com/AlexxIT/go2rtc/pkg/hap/camera" "github.com/AlexxIT/go2rtc/pkg/hap/hds" - "github.com/AlexxIT/go2rtc/pkg/hap/secure" "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) -func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFunc { +type ServerProxy interface { + ServerPair + AddConn(conn any) + DelConn(conn any) +} + +func ProxyHandler(srv ServerProxy, acc net.Conn) HandlerFunc { return func(con net.Conn) error { defer con.Close() - acc, err := dial() - if err != nil { - return err - } - defer acc.Close() - pr := &Proxy{ - con: con.(*secure.Conn), - acc: acc.(*secure.Conn), + con: con.(*hap.Conn), + acc: acc.(*hap.Conn), res: make(chan *http.Response), } @@ -36,17 +35,17 @@ func ProxyHandler(pair ServerPair, dial func() (net.Conn, error)) hap.HandlerFun go pr.handleAcc() // controller => accessory - return pr.handleCon(pair) + return pr.handleCon(srv) } } type Proxy struct { - con *secure.Conn - acc *secure.Conn + con *hap.Conn + acc *hap.Conn res chan *http.Response } -func (p *Proxy) handleCon(pair ServerPair) error { +func (p *Proxy) handleCon(srv ServerProxy) error { var hdsCharIID uint64 rd := bufio.NewReader(p.con) @@ -61,7 +60,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { switch { case req.Method == "POST" && req.URL.Path == hap.PathPairings: var res *http.Response - if res, err = handlePairings(p.con, req, pair); err != nil { + if res, err = handlePairings(req, srv); err != nil { return err } if err = res.Write(p.con); err != nil { @@ -74,7 +73,7 @@ func (p *Proxy) handleCon(pair ServerPair) error { _ = json.Unmarshal(body, &v) for _, char := range v.Value { if char.IID == hdsCharIID { - var hdsReq camera.SetupDataStreamRequest + var hdsReq camera.SetupDataStreamTransportRequest _ = tlv8.UnmarshalBase64(char.Value, &hdsReq) hdsConSalt = hdsReq.ControllerKeySalt break @@ -110,14 +109,14 @@ func (p *Proxy) handleCon(pair ServerPair) error { _ = json.Unmarshal(body, &v) for i, char := range v.Value { if char.IID == hdsCharIID { - var hdsRes camera.SetupDataStreamResponse + var hdsRes camera.SetupDataStreamTransportResponse _ = tlv8.UnmarshalBase64(char.Value, &hdsRes) hdsAccSalt := hdsRes.AccessoryKeySalt hdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort) // swtich accPort to conPort - hdsPort, err = p.listenHDS(hdsPort, hdsConSalt+hdsAccSalt) + hdsPort, err = p.listenHDS(srv, hdsPort, hdsConSalt+hdsAccSalt) if err != nil { return err } @@ -149,7 +148,7 @@ func (p *Proxy) handleAcc() error { } if res.Proto == hap.ProtoEvent { - if err = res.Write(p.con); err != nil { + if err = hap.WriteEvent(p.con, res); err != nil { return err } continue @@ -166,7 +165,8 @@ func (p *Proxy) handleAcc() error { } } -func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { +func (p *Proxy) listenHDS(srv ServerProxy, accPort int, salt string) (int, error) { + // The TCP port range for HDS must be >= 32768. ln, err := net.ListenTCP("tcp", nil) if err != nil { return 0, err @@ -175,30 +175,36 @@ func (p *Proxy) listenHDS(accPort int, salt string) (int, error) { go func() { defer ln.Close() + _ = ln.SetDeadline(time.Now().Add(30 * time.Second)) + // raw controller conn - con, err := ln.Accept() + conn1, err := ln.Accept() if err != nil { return } - defer con.Close() + + defer conn1.Close() // secured controller conn (controlle=false because we are accessory) - con, err = hds.Client(con, p.con.SharedKey, salt, false) + con, err := hds.NewConn(conn1, p.con.SharedKey, salt, false) if err != nil { return } + srv.AddConn(con) + defer srv.DelConn(con) + accIP := p.acc.RemoteAddr().(*net.TCPAddr).IP // raw accessory conn - acc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", accIP, accPort)) + conn2, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: accIP, Port: accPort}) if err != nil { return } - defer acc.Close() + defer conn2.Close() // secured accessory conn (controller=true because we are controller) - acc, err = hds.Client(acc, p.acc.SharedKey, salt, true) + acc, err := hds.NewConn(conn2, p.acc.SharedKey, salt, true) if err != nil { return } diff --git a/pkg/homekit/server.go b/pkg/homekit/server.go index 20cfc59d..75ba2a0f 100644 --- a/pkg/homekit/server.go +++ b/pkg/homekit/server.go @@ -15,15 +15,17 @@ import ( "github.com/AlexxIT/go2rtc/pkg/hap/tlv8" ) +type HandlerFunc func(net.Conn) error + type Server interface { ServerPair ServerAccessory } type ServerPair interface { - GetPair(conn net.Conn, id string) []byte - AddPair(conn net.Conn, id string, public []byte, permissions byte) - DelPair(conn net.Conn, id string) + GetPair(id string) []byte + AddPair(id string, public []byte, permissions byte) + DelPair(id string) } type ServerAccessory interface { @@ -33,11 +35,11 @@ type ServerAccessory interface { GetImage(conn net.Conn, width, height int) []byte } -func ServerHandler(server Server) hap.HandlerFunc { +func ServerHandler(server Server) HandlerFunc { return handleRequest(func(conn net.Conn, req *http.Request) (*http.Response, error) { switch req.URL.Path { case hap.PathPairings: - return handlePairings(conn, req, server) + return handlePairings(req, server) case hap.PathAccessories: body := hap.JSONAccessories{Value: server.GetAccessories(conn)} @@ -103,7 +105,7 @@ func ServerHandler(server Server) hap.HandlerFunc { }) } -func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) hap.HandlerFunc { +func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) HandlerFunc { return func(conn net.Conn) error { rw := bufio.NewReaderSize(conn, 16*1024) wr := bufio.NewWriterSize(conn, 16*1024) @@ -130,7 +132,7 @@ func handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response } } -func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Response, error) { +func handlePairings(req *http.Request, srv ServerPair) (*http.Response, error) { cmd := struct { Method byte `tlv8:"0"` Identifier string `tlv8:"1"` @@ -139,15 +141,15 @@ func handlePairings(conn net.Conn, req *http.Request, pair ServerPair) (*http.Re Permissions byte `tlv8:"11"` }{} - if err := tlv8.UnmarshalReader(req.Body, &cmd); err != nil { + if err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &cmd); err != nil { return nil, err } switch cmd.Method { case 3: // add - pair.AddPair(conn, cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) + srv.AddPair(cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions) case 4: // delete - pair.DelPair(conn, cmd.Identifier) + srv.DelPair(cmd.Identifier) } body := struct { @@ -190,40 +192,3 @@ func makeResponse(mime string, v any) (*http.Response, error) { } return res, nil } - -//func debug(v any) { -// switch v := v.(type) { -// case *http.Request: -// if v == nil { -// return -// } -// if v.ContentLength != 0 { -// b, err := io.ReadAll(v.Body) -// if err != nil { -// panic(err) -// } -// v.Body = io.NopCloser(bytes.NewReader(b)) -// log.Printf("[homekit] request: %s %s\n%s", v.Method, v.RequestURI, b) -// } else { -// log.Printf("[homekit] request: %s %s ", v.Method, v.RequestURI) -// } -// case *http.Response: -// if v == nil { -// return -// } -// if v.Header.Get("Content-Type") == "image/jpeg" { -// log.Printf("[homekit] response: %d ", v.StatusCode) -// return -// } -// if v.ContentLength != 0 { -// b, err := io.ReadAll(v.Body) -// if err != nil { -// panic(err) -// } -// v.Body = io.NopCloser(bytes.NewReader(b)) -// log.Printf("[homekit] response: %d\n%s", v.StatusCode, b) -// } else { -// log.Printf("[homekit] response: %d ", v.StatusCode) -// } -// } -//} diff --git a/www/add.html b/www/add.html index 2d85d508..325df646 100644 --- a/www/add.html +++ b/www/add.html @@ -1,41 +1,37 @@ + + go2rtc - Add Stream - - + + - - -
-
- - - -
-
- - - - -
-
-
- - - - -
-
- - - - -
-
- - -
-
-
- + const r = await fetch(url, {method: 'PUT'}); + alert(r.ok ? 'OK' : 'ERROR: ' + await r.text()); + }); + - -
-
-
- + +
+
+
+ - -
-
-
- + +
+
+ + + + +
+
+ + +
+
+
+ - - - -
-
- - - - - -
-
-
- - - -
-
- - - - -
-
- - -
-
-
- + + + +
+
+
+ + + + +
+
+
+ + + + +
+
+
+ + + + +
+
+ + + + + +
+
+
+ + + +
+
+
+ + + + +
+
+
+ + + + +
+
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + +
+
+
+ - - -
-
- - - - -
- -
-
- - - -
-
-
- + document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth); + document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth); + - -
-
-
- + +
+
+ + + +
+ +
+
+ - -
-
- - -
-
-
- + +
+
+
+ - -
-
- - - -
- -
-
- - - - -
-
-
- - - - -
-
-
- - + +
+
+
+ + - + \ No newline at end of file diff --git a/www/editor.html b/www/config.html similarity index 75% rename from www/editor.html rename to www/config.html index cb455f4d..9e1853c2 100644 --- a/www/editor.html +++ b/www/config.html @@ -1,41 +1,36 @@ - go2rtc - File Editor - - + + + go2rtc - Config - -
- -
-
-
- + +
+
+ +
+
+
+ + + diff --git a/www/index.html b/www/index.html index 63fedcec..69126e6f 100644 --- a/www/index.html +++ b/www/index.html @@ -1,61 +1,49 @@ - - - - - - + + go2rtc + -
-
- - - - - -
- - - - - - - - - - -
OnlineCommands
+ +
+
+ + modes + + + + +
+ + + + + + + + + + +
onlinecommands
+
+
+ + diff --git a/www/links.html b/www/links.html index 3b651762..36cb7a7e 100644 --- a/www/links.html +++ b/www/links.html @@ -1,27 +1,10 @@ + + go2rtc - links - - - - - + +
+ + + }); + -
-

Play audio

-
-
-
-
- - send / cameras with two way audio support -
- + + + +
+

Play audio

+ + + +
+ + + / cameras with two way audio support +
+ + +
+

Publish stream

+
YouTube:  rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx
 Telegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx
- - send / Telegram RTMPS server -
- + + + / Telegram RTMPS server + + -
-

WebRTC Magic

-
-
-
-
+
+

WebRTC Magic

+ + + + -
-
  • webrtc.html local WebRTC viewer
  • +
    +
  • webrtc.html local WebRTC viewer
  • -
  • - share link - copy link - delete - external WebRTC viewer -
  • -
    - + +
    diff --git a/www/log.html b/www/log.html index 84ec0675..67476603 100644 --- a/www/log.html +++ b/www/log.html @@ -1,69 +1,64 @@ + + go2rtc - Logs - - + -
    - - - -
    -
    - - - - - - - - - - -
    TimeLevelMessage
    + +
    +
    + + + +
    + + + + + + + + + + +
    TimeLevelMessage
    +
    + + diff --git a/www/main.js b/www/main.js index 714c9127..c901f300 100644 --- a/www/main.js +++ b/www/main.js @@ -1,200 +1,135 @@ -// main menu -document.body.innerHTML = ` +document.head.innerHTML += ` - +`; + +document.body.innerHTML = ` +
    + +
    ` + document.body.innerHTML; - -const sunIcon = '☀️'; -const moonIcon = '🌕'; - -document.addEventListener('DOMContentLoaded', () => { - const darkModeToggle = document.getElementById('darkModeToggle'); - const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); - - const isDarkModeEnabled = () => document.body.classList.contains('dark-mode'); - - // Update the toggle button based on the dark mode state - const updateToggleButton = () => { - if (isDarkModeEnabled()) { - darkModeToggle.innerHTML = sunIcon; - darkModeToggle.setAttribute('aria-label', 'Enable light mode'); - } else { - darkModeToggle.innerHTML = moonIcon; - darkModeToggle.setAttribute('aria-label', 'Enable dark mode'); - } - }; - - const updateDarkMode = () => { - if (localStorage.getItem('darkMode') === 'enabled' || prefersDarkScheme.matches && localStorage.getItem('darkMode') !== 'disabled') { - document.body.classList.add('dark-mode'); - } else { - document.body.classList.remove('dark-mode'); - } - updateEditorTheme(); - updateToggleButton(); - }; - - // Update the editor theme based on the dark mode state - const updateEditorTheme = () => { - if (typeof editor !== 'undefined') { - editor.setTheme(isDarkModeEnabled() ? 'ace/theme/tomorrow_night_eighties' : 'ace/theme/github'); - } - }; - - // Initial update for dark mode and toggle button - updateDarkMode(); - - // Listen for changes in the system's color scheme preference - prefersDarkScheme.addEventListener('change', updateDarkMode); // Modern approach - - // Toggle dark mode and update local storage on button click - darkModeToggle.addEventListener('click', () => { - const enabled = document.body.classList.toggle('dark-mode'); - localStorage.setItem('darkMode', enabled ? 'enabled' : 'disabled'); - updateToggleButton(); // Update the button after toggling - updateEditorTheme(); - }); -}); diff --git a/www/network.html b/www/net.html similarity index 87% rename from www/network.html rename to www/net.html index 79875012..8f0f91ac 100644 --- a/www/network.html +++ b/www/net.html @@ -2,31 +2,21 @@ + go2rtc - Network -
    + + +
    + +