Add credential extraction registry for generate
Protocols like Xiaomi need credentials (tokens) in a separate top-level YAML section, not in the stream URL itself. Introduce a registry pattern mirroring streams.HandleFunc / tester.RegisterSource: - pkg/generate/registry.go: ExtractFunc + RegisterExtract - Extractors clean the URL (strip ?token=...) and return section/key/value - writeCredentials emits sorted sections between go2rtc: and cameras: - upsertCredentials in addToConfig merges into existing sections: * replaces value if key exists (token refresh) * inserts in sorted order if new * creates new top-level section before cameras: if missing Xiaomi registers its extractor from internal/xiaomi/xiaomi.go. Adding Tapo/Ring/Roborock later is one line + a small function in their internal/*/ module -- zero changes in pkg/generate/.
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto"
|
||||
"github.com/eduard256/strix/internal/api"
|
||||
"github.com/eduard256/strix/internal/app"
|
||||
"github.com/eduard256/strix/pkg/generate"
|
||||
"github.com/eduard256/strix/pkg/tester"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -53,6 +54,29 @@ func Init() {
|
||||
})
|
||||
|
||||
api.HandleFunc("api/xiaomi", apiXiaomi)
|
||||
|
||||
generate.RegisterExtract("xiaomi", extractForConfig)
|
||||
}
|
||||
|
||||
// extractForConfig strips ?token=... from xiaomi:// URL and returns
|
||||
// userID + token for a top-level `xiaomi:` yaml section.
|
||||
// ex. xiaomi://4161148305:cn@10.0.20.229?did=450924912&model=X&token=V1:...
|
||||
// -> xiaomi://4161148305:cn@10.0.20.229?did=450924912&model=X, "xiaomi", "4161148305", "V1:..."
|
||||
func extractForConfig(rawURL string) (cleaned, section, key, value string) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil || u.User == nil {
|
||||
return rawURL, "", "", ""
|
||||
}
|
||||
|
||||
q := u.Query()
|
||||
token := q.Get("token")
|
||||
if token == "" {
|
||||
return rawURL, "", "", ""
|
||||
}
|
||||
q.Del("token")
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
return u.String(), "xiaomi", u.User.Username(), token
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
+36
-4
@@ -54,10 +54,16 @@ func buildInfo(req *Request) *cameraInfo {
|
||||
streamBase = sanitized
|
||||
}
|
||||
|
||||
mainSource, mainSection, mainKey, mainValue := runExtract(req.MainStream)
|
||||
|
||||
info := &cameraInfo{
|
||||
CameraName: base,
|
||||
MainStreamName: streamBase + "_main",
|
||||
MainSource: req.MainStream,
|
||||
MainSource: mainSource,
|
||||
}
|
||||
|
||||
if mainSection != "" {
|
||||
info.addCredential(mainSection, mainKey, mainValue)
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
@@ -70,7 +76,11 @@ func buildInfo(req *Request) *cameraInfo {
|
||||
info.MainStreamName = req.Go2RTC.MainStreamName
|
||||
}
|
||||
if req.Go2RTC.MainStreamSource != "" {
|
||||
info.MainSource = req.Go2RTC.MainStreamSource
|
||||
src, section, key, value := runExtract(req.Go2RTC.MainStreamSource)
|
||||
info.MainSource = src
|
||||
if section != "" {
|
||||
info.addCredential(section, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +105,12 @@ func buildInfo(req *Request) *cameraInfo {
|
||||
if req.Name != "" {
|
||||
subName = req.Name + "_sub"
|
||||
}
|
||||
subSource := req.SubStream
|
||||
|
||||
subSource, subSection, subKey, subValue := runExtract(req.SubStream)
|
||||
if subSection != "" {
|
||||
info.addCredential(subSection, subKey, subValue)
|
||||
}
|
||||
|
||||
subPath := "rtsp://127.0.0.1:8554/" + subName
|
||||
if needMP4[subScheme] {
|
||||
subPath += "?mp4"
|
||||
@@ -107,7 +122,11 @@ func buildInfo(req *Request) *cameraInfo {
|
||||
subName = req.Go2RTC.SubStreamName
|
||||
}
|
||||
if req.Go2RTC.SubStreamSource != "" {
|
||||
subSource = req.Go2RTC.SubStreamSource
|
||||
src, section, key, value := runExtract(req.Go2RTC.SubStreamSource)
|
||||
subSource = src
|
||||
if section != "" {
|
||||
info.addCredential(section, key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Frigate != nil {
|
||||
@@ -137,6 +156,8 @@ func newConfig(info *cameraInfo, req *Request) string {
|
||||
b.WriteString("go2rtc:\n streams:\n")
|
||||
writeStreamLines(&b, info)
|
||||
|
||||
writeCredentials(&b, info.Credentials)
|
||||
|
||||
b.WriteString("cameras:\n")
|
||||
writeCameraBlock(&b, info, req)
|
||||
|
||||
@@ -156,6 +177,17 @@ type cameraInfo struct {
|
||||
SubSource string
|
||||
SubPath string
|
||||
SubInputArgs string
|
||||
Credentials map[string]map[string]string // section -> key -> value
|
||||
}
|
||||
|
||||
func (c *cameraInfo) addCredential(section, key, value string) {
|
||||
if c.Credentials == nil {
|
||||
c.Credentials = map[string]map[string]string{}
|
||||
}
|
||||
if c.Credentials[section] == nil {
|
||||
c.Credentials[section] = map[string]string{}
|
||||
}
|
||||
c.Credentials[section][key] = value
|
||||
}
|
||||
|
||||
func urlScheme(rawURL string) string {
|
||||
|
||||
@@ -3,6 +3,7 @@ package generate
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -64,6 +65,9 @@ func addToConfig(existing string, info *cameraInfo, req *Request) (*Response, er
|
||||
}
|
||||
result = append(result, rest[split:]...)
|
||||
|
||||
// upsert credential sections (xiaomi, tapo, ...) before cameras:
|
||||
result, added = upsertCredentials(result, info.Credentials, added)
|
||||
|
||||
config := strings.Join(result, "\n")
|
||||
|
||||
addedLines := make([]int, 0, len(added))
|
||||
@@ -156,6 +160,156 @@ func findStreamInsertPoint(lines []string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// upsertCredentials merges creds into existing top-level sections. For each
|
||||
// section: if a matching line ` "<key>":` exists -- replace its value; else
|
||||
// insert in sorted order. If the section itself doesn't exist -- create a new
|
||||
// top-level block just before `cameras:`.
|
||||
func upsertCredentials(lines []string, creds map[string]map[string]string, added map[int]bool) ([]string, map[int]bool) {
|
||||
if len(creds) == 0 {
|
||||
return lines, added
|
||||
}
|
||||
|
||||
sections := make([]string, 0, len(creds))
|
||||
for s := range creds {
|
||||
sections = append(sections, s)
|
||||
}
|
||||
sort.Strings(sections)
|
||||
|
||||
for _, section := range sections {
|
||||
lines, added = upsertSection(lines, section, creds[section], added)
|
||||
}
|
||||
|
||||
return lines, added
|
||||
}
|
||||
|
||||
// upsertSection updates or appends a single top-level section.
|
||||
var reCredKey = regexp.MustCompile(`^\s{2}"([^"]+)":`)
|
||||
|
||||
func upsertSection(lines []string, section string, kv map[string]string, added map[int]bool) ([]string, map[int]bool) {
|
||||
reHeader := regexp.MustCompile(`^` + regexp.QuoteMeta(section) + `:\s*$`)
|
||||
|
||||
headerIdx := -1
|
||||
for i, line := range lines {
|
||||
if reHeader.MatchString(line) {
|
||||
headerIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if headerIdx == -1 {
|
||||
return insertNewSection(lines, section, kv, added)
|
||||
}
|
||||
|
||||
// section exists -- find last content line of the section (skip trailing blanks)
|
||||
end := len(lines)
|
||||
for i := headerIdx + 1; i < len(lines); i++ {
|
||||
if strings.TrimSpace(lines[i]) == "" || reTopLevel.MatchString(lines[i]) {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(kv))
|
||||
for k := range kv {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
newLine := fmt.Sprintf(" %q: %s", k, kv[k])
|
||||
|
||||
// try replace -- no length change, just mark modified line as added
|
||||
replaced := false
|
||||
for i := headerIdx + 1; i < end; i++ {
|
||||
if m := reCredKey.FindStringSubmatch(lines[i]); m != nil && m[1] == k {
|
||||
if lines[i] != newLine {
|
||||
lines[i] = newLine
|
||||
added[i] = true
|
||||
}
|
||||
replaced = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if replaced {
|
||||
continue
|
||||
}
|
||||
|
||||
// insert in sorted order within section
|
||||
insertAt := headerIdx + 1
|
||||
for i := headerIdx + 1; i < end; i++ {
|
||||
if m := reCredKey.FindStringSubmatch(lines[i]); m != nil {
|
||||
if m[1] < k {
|
||||
insertAt = i + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
insertAt = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
lines = append(lines[:insertAt], append([]string{newLine}, lines[insertAt:]...)...)
|
||||
added = shiftAdded(added, insertAt)
|
||||
added[insertAt] = true
|
||||
end++
|
||||
}
|
||||
|
||||
return lines, added
|
||||
}
|
||||
|
||||
func insertNewSection(lines []string, section string, kv map[string]string, added map[int]bool) ([]string, map[int]bool) {
|
||||
camIdx := -1
|
||||
for i, line := range lines {
|
||||
if reCamerasHeader.MatchString(line) {
|
||||
camIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if camIdx == -1 {
|
||||
camIdx = len(lines)
|
||||
}
|
||||
|
||||
// insert point: right before the blank line that precedes cameras:
|
||||
// keep one blank line between blocks
|
||||
insertAt := camIdx
|
||||
for insertAt > 0 && strings.TrimSpace(lines[insertAt-1]) == "" {
|
||||
insertAt--
|
||||
}
|
||||
|
||||
block := []string{section + ":"}
|
||||
keys := make([]string, 0, len(kv))
|
||||
for k := range kv {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
block = append(block, fmt.Sprintf(" %q: %s", k, kv[k]))
|
||||
}
|
||||
block = append(block, "")
|
||||
|
||||
lines = append(lines[:insertAt], append(block, lines[insertAt:]...)...)
|
||||
added = shiftAdded(added, insertAt)
|
||||
for i := range block {
|
||||
added[insertAt+i] = true
|
||||
}
|
||||
|
||||
return lines, added
|
||||
}
|
||||
|
||||
// shiftAdded moves all marks at index >= from by +1. Also used with from=len(lines)
|
||||
// as a no-op shift (just return same map).
|
||||
func shiftAdded(added map[int]bool, from int) map[int]bool {
|
||||
out := make(map[int]bool, len(added))
|
||||
for i := range added {
|
||||
if i >= from {
|
||||
out[i+1] = true
|
||||
} else {
|
||||
out[i] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func findCameraInsertPoint(lines []string) int {
|
||||
in := false
|
||||
last := -1
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package generate
|
||||
|
||||
import "strings"
|
||||
|
||||
// ExtractFunc cleans rawURL (ex. strips ?token=...) and returns a top-level
|
||||
// YAML section name + key/value to upsert into the config.
|
||||
// Returns empty section if the URL has nothing to extract -- cleaned URL
|
||||
// is still used as-is.
|
||||
type ExtractFunc func(rawURL string) (cleaned, section, key, value string)
|
||||
|
||||
var extractors = map[string]ExtractFunc{}
|
||||
|
||||
func RegisterExtract(scheme string, fn ExtractFunc) {
|
||||
extractors[scheme] = fn
|
||||
}
|
||||
|
||||
func runExtract(rawURL string) (cleaned, section, key, value string) {
|
||||
if i := strings.IndexByte(rawURL, ':'); i > 0 {
|
||||
if fn := extractors[rawURL[:i]]; fn != nil {
|
||||
return fn(rawURL)
|
||||
}
|
||||
}
|
||||
return rawURL, "", "", ""
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package generate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -17,6 +18,38 @@ func writeStreamLines(b *strings.Builder, info *cameraInfo) {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
|
||||
// writeCredentials writes top-level credential sections (xiaomi, tapo, ring, ...)
|
||||
// populated by registered ExtractFunc handlers. Sorted by section, then by key.
|
||||
// ex.
|
||||
// xiaomi:
|
||||
// "4161148305": V1:9d2w...
|
||||
func writeCredentials(b *strings.Builder, creds map[string]map[string]string) {
|
||||
if len(creds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sections := make([]string, 0, len(creds))
|
||||
for s := range creds {
|
||||
sections = append(sections, s)
|
||||
}
|
||||
sort.Strings(sections)
|
||||
|
||||
for _, section := range sections {
|
||||
fmt.Fprintf(b, "%s:\n", section)
|
||||
|
||||
keys := make([]string, 0, len(creds[section]))
|
||||
for k := range creds[section] {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(b, " %q: %s\n", k, creds[section][k])
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func writeCameraBlock(b *strings.Builder, info *cameraInfo, req *Request) {
|
||||
fmt.Fprintf(b, " %s:\n", info.CameraName)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user