Merge branch 'master' of https://github.com/AlexxIT/go2rtc into tuya-new
This commit is contained in:
+17
-3
@@ -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,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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -25,4 +25,5 @@ func Init() {
|
||||
|
||||
return url, nil
|
||||
})
|
||||
streams.MarkInsecure("expr")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
Reference in New Issue
Block a user