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/AlexxIT/go2rtc/pkg/xiaomi/crypto"
|
||||||
"github.com/eduard256/strix/internal/api"
|
"github.com/eduard256/strix/internal/api"
|
||||||
"github.com/eduard256/strix/internal/app"
|
"github.com/eduard256/strix/internal/app"
|
||||||
|
"github.com/eduard256/strix/pkg/generate"
|
||||||
"github.com/eduard256/strix/pkg/tester"
|
"github.com/eduard256/strix/pkg/tester"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
@@ -53,6 +54,29 @@ func Init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.HandleFunc("api/xiaomi", apiXiaomi)
|
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
|
var log zerolog.Logger
|
||||||
|
|||||||
+36
-4
@@ -54,10 +54,16 @@ func buildInfo(req *Request) *cameraInfo {
|
|||||||
streamBase = sanitized
|
streamBase = sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainSource, mainSection, mainKey, mainValue := runExtract(req.MainStream)
|
||||||
|
|
||||||
info := &cameraInfo{
|
info := &cameraInfo{
|
||||||
CameraName: base,
|
CameraName: base,
|
||||||
MainStreamName: streamBase + "_main",
|
MainStreamName: streamBase + "_main",
|
||||||
MainSource: req.MainStream,
|
MainSource: mainSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mainSection != "" {
|
||||||
|
info.addCredential(mainSection, mainKey, mainValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Name != "" {
|
if req.Name != "" {
|
||||||
@@ -70,7 +76,11 @@ func buildInfo(req *Request) *cameraInfo {
|
|||||||
info.MainStreamName = req.Go2RTC.MainStreamName
|
info.MainStreamName = req.Go2RTC.MainStreamName
|
||||||
}
|
}
|
||||||
if req.Go2RTC.MainStreamSource != "" {
|
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 != "" {
|
if req.Name != "" {
|
||||||
subName = req.Name + "_sub"
|
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
|
subPath := "rtsp://127.0.0.1:8554/" + subName
|
||||||
if needMP4[subScheme] {
|
if needMP4[subScheme] {
|
||||||
subPath += "?mp4"
|
subPath += "?mp4"
|
||||||
@@ -107,7 +122,11 @@ func buildInfo(req *Request) *cameraInfo {
|
|||||||
subName = req.Go2RTC.SubStreamName
|
subName = req.Go2RTC.SubStreamName
|
||||||
}
|
}
|
||||||
if req.Go2RTC.SubStreamSource != "" {
|
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 {
|
if req.Frigate != nil {
|
||||||
@@ -137,6 +156,8 @@ func newConfig(info *cameraInfo, req *Request) string {
|
|||||||
b.WriteString("go2rtc:\n streams:\n")
|
b.WriteString("go2rtc:\n streams:\n")
|
||||||
writeStreamLines(&b, info)
|
writeStreamLines(&b, info)
|
||||||
|
|
||||||
|
writeCredentials(&b, info.Credentials)
|
||||||
|
|
||||||
b.WriteString("cameras:\n")
|
b.WriteString("cameras:\n")
|
||||||
writeCameraBlock(&b, info, req)
|
writeCameraBlock(&b, info, req)
|
||||||
|
|
||||||
@@ -156,6 +177,17 @@ type cameraInfo struct {
|
|||||||
SubSource string
|
SubSource string
|
||||||
SubPath string
|
SubPath string
|
||||||
SubInputArgs 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 {
|
func urlScheme(rawURL string) string {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package generate
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,6 +65,9 @@ func addToConfig(existing string, info *cameraInfo, req *Request) (*Response, er
|
|||||||
}
|
}
|
||||||
result = append(result, rest[split:]...)
|
result = append(result, rest[split:]...)
|
||||||
|
|
||||||
|
// upsert credential sections (xiaomi, tapo, ...) before cameras:
|
||||||
|
result, added = upsertCredentials(result, info.Credentials, added)
|
||||||
|
|
||||||
config := strings.Join(result, "\n")
|
config := strings.Join(result, "\n")
|
||||||
|
|
||||||
addedLines := make([]int, 0, len(added))
|
addedLines := make([]int, 0, len(added))
|
||||||
@@ -156,6 +160,156 @@ func findStreamInsertPoint(lines []string) int {
|
|||||||
return -1
|
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 {
|
func findCameraInsertPoint(lines []string) int {
|
||||||
in := false
|
in := false
|
||||||
last := -1
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,6 +18,38 @@ func writeStreamLines(b *strings.Builder, info *cameraInfo) {
|
|||||||
b.WriteByte('\n')
|
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) {
|
func writeCameraBlock(b *strings.Builder, info *cameraInfo, req *Request) {
|
||||||
fmt.Fprintf(b, " %s:\n", info.CameraName)
|
fmt.Fprintf(b, " %s:\n", info.CameraName)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user