Merge branch 'master' of https://github.com/AlexxIT/go2rtc into tuya-new

This commit is contained in:
seydx
2025-11-19 00:09:34 +01:00
64 changed files with 2211 additions and 1714 deletions
+17 -3
View File
@@ -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"`)
+11
View File
@@ -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) {
+17
View File
@@ -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")
}
+18
View File
@@ -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 {
+1
View File
@@ -25,4 +25,5 @@ func Init() {
return url, nil
})
streams.MarkInsecure("expr")
}
+2 -2
View File
@@ -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
+8 -2
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+79 -37
View File
@@ -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
+33 -54
View File
@@ -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
}
}
+235 -95
View File
@@ -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
}
+2 -2
View File
@@ -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)
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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)
}
+4
View File
@@ -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)
}
+8 -4
View File
@@ -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())
}
+66
View File
@@ -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")
}
+37
View File
@@ -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
}
+20 -28
View File
@@ -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)
}
+1 -1
View File
@@ -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 != "" {