From 18aaf07eca18695ce1735f73628c2a7d956efa84 Mon Sep 17 00:00:00 2001 From: eduard256 Date: Fri, 7 Nov 2025 21:33:47 +0300 Subject: [PATCH] Fix HTTP camera authentication: generate all auth variants for comprehensive testing This commit addresses the issue where HTTP-based cameras (JPEG/MJPEG) were not being discovered due to incomplete authentication variant generation. Changes: - Builder now generates 4 authentication variants for each HTTP/HTTPS URL: 1. No authentication (for open cameras) 2. Basic Auth only (embedded credentials in URL) 3. Query parameters only (user=X&pwd=Y) 4. Basic Auth + Query parameters (combined approach) - Scanner updated to use BuildURLsFromEntry instead of BuildURL for popular patterns, ensuring all authentication variants are tested - Preserved existing query parameter values (e.g., channel=1 remains unchanged) This fix enables discovery of cameras that require different authentication methods, including those that only accept Basic Auth headers, query parameters, or combinations. Tested with ZOSI ZG23213M camera - increased discovery from 0 to 9 working streams. --- internal/camera/discovery/scanner.go | 17 ++-- internal/camera/stream/builder.go | 115 +++++++++++++++++---------- 2 files changed, 85 insertions(+), 47 deletions(-) diff --git a/internal/camera/discovery/scanner.go b/internal/camera/discovery/scanner.go index 045d0d5..82a185f 100644 --- a/internal/camera/discovery/scanner.go +++ b/internal/camera/discovery/scanner.go @@ -362,17 +362,20 @@ func (s *Scanner) collectStreams(ctx context.Context, req models.StreamDiscovery buildCtx.Port = pattern.Port buildCtx.Protocol = pattern.Protocol - url := s.builder.BuildURL(entry, buildCtx) - if !urlMap[url] { - allStreams = append(allStreams, models.DiscoveredStream{ - URL: url, - Type: pattern.Type, + // Generate all URL variants for this pattern + urls := s.builder.BuildURLsFromEntry(entry, buildCtx) + for _, url := range urls { + if !urlMap[url] { + allStreams = append(allStreams, models.DiscoveredStream{ + URL: url, + Type: pattern.Type, Protocol: pattern.Protocol, Port: pattern.Port, Working: false, // Will be tested }) - urlMap[url] = true - popularCount++ + urlMap[url] = true + popularCount++ + } } } } diff --git a/internal/camera/stream/builder.go b/internal/camera/stream/builder.go index 683da0f..7fb85ee 100644 --- a/internal/camera/stream/builder.go +++ b/internal/camera/stream/builder.go @@ -300,41 +300,95 @@ func (b *Builder) BuildURLsFromEntry(entry models.CameraEntry, ctx BuildContext) switch entry.Protocol { case "rtsp", "rtsps": // For RTSP: generate with and without credentials - // 1. With credentials (if provided) if ctx.Username != "" && ctx.Password != "" { addURL(b.BuildURL(entry, ctx)) } - - // 2. Without credentials (for open cameras) + // Without credentials (for open cameras) ctxNoAuth := ctx ctxNoAuth.Username = "" ctxNoAuth.Password = "" addURL(b.BuildURL(entry, ctxNoAuth)) case "http", "https": - // For HTTP/JPEG/MJPEG: generate multiple auth variants - if entry.Type == "JPEG" || entry.Type == "MJPEG" { - // Check if URL has auth placeholders - hasAuthPlaceholders := strings.Contains(entry.URL, "[USERNAME]") || - strings.Contains(entry.URL, "[PASSWORD]") || - strings.Contains(entry.URL, "[AUTH]") + // For HTTP/HTTPS: ALWAYS generate 4 authentication variants + if ctx.Username != "" && ctx.Password != "" { + // 1. No authentication + ctxNoAuth := ctx + ctxNoAuth.Username = "" + ctxNoAuth.Password = "" + urlNoAuth := b.BuildURL(entry, ctxNoAuth) + addURL(urlNoAuth) - if hasAuthPlaceholders { - // 1. URL with credentials in parameters (replaced placeholders) - addURL(b.BuildURL(entry, ctx)) + // 2. Basic Auth only (embedded credentials) + urlBasic := b.BuildURL(entry, ctxNoAuth) // Use clean URL + if u, err := url.Parse(urlBasic); err == nil { + u.User = url.UserPassword(ctx.Username, ctx.Password) + addURL(u.String()) + } - // 2. URL without credentials (for cameras that don't require auth) - ctxNoAuth := ctx - ctxNoAuth.Username = "" - ctxNoAuth.Password = "" - addURL(b.BuildURL(entry, ctxNoAuth)) + // 3. Query parameters only + urlWithParams := b.BuildURL(entry, ctx) // This will replace placeholders if any + + // If URL has auth placeholders, they're already replaced + if strings.Contains(entry.URL, "[USERNAME]") || strings.Contains(entry.URL, "[PASSWORD]") { + addURL(urlWithParams) } else { - // URL without placeholders - will use Basic Auth in headers - // Generate only one URL, auth will be in headers - addURL(b.BuildURL(entry, ctx)) + // No placeholders - add query params for auth (don't overwrite existing params) + if u, err := url.Parse(urlWithParams); err == nil { + q := u.Query() + + // Add user/pwd if not already present + if !q.Has("user") && !q.Has("usr") && !q.Has("username") { + q.Set("user", ctx.Username) + } + if !q.Has("pwd") && !q.Has("password") && !q.Has("pass") { + q.Set("pwd", ctx.Password) + } + u.RawQuery = q.Encode() + addURL(u.String()) + + // Try alternative names too + q2 := url.Values{} + for k, v := range u.Query() { + q2[k] = v + } + if !q2.Has("username") && !q2.Has("user") && !q2.Has("usr") { + q2.Set("username", ctx.Username) + } + if !q2.Has("password") && !q2.Has("pwd") && !q2.Has("pass") { + q2.Set("password", ctx.Password) + } + u.RawQuery = q2.Encode() + addURL(u.String()) + } + } + + // 4. Basic Auth + Query parameters (combined) + if strings.Contains(entry.URL, "[USERNAME]") || strings.Contains(entry.URL, "[PASSWORD]") { + // URL has placeholders - add Basic Auth to the URL with replaced params + if u, err := url.Parse(urlWithParams); err == nil { + u.User = url.UserPassword(ctx.Username, ctx.Password) + addURL(u.String()) + } + } else { + // No placeholders - add both Basic Auth and query params (without overwriting existing) + if u, err := url.Parse(urlNoAuth); err == nil { + u.User = url.UserPassword(ctx.Username, ctx.Password) + q := u.Query() + + // Add auth params only if not already present + if !q.Has("user") && !q.Has("usr") && !q.Has("username") { + q.Set("user", ctx.Username) + } + if !q.Has("pwd") && !q.Has("password") && !q.Has("pass") { + q.Set("pwd", ctx.Password) + } + u.RawQuery = q.Encode() + addURL(u.String()) + } } } else { - // Other HTTP types - single URL + // No credentials provided - just one URL addURL(b.BuildURL(entry, ctx)) } @@ -343,25 +397,6 @@ func (b *Builder) BuildURLsFromEntry(entry models.CameraEntry, ctx BuildContext) addURL(b.BuildURL(entry, ctx)) } - // For NVR systems, try multiple channels - if ctx.Channel == 0 && strings.Contains(strings.ToLower(entry.Notes), "channel") { - for ch := 1; ch <= 4; ch++ { - altCtx := ctx - altCtx.Channel = ch - - // Regenerate with different channel - if entry.Protocol == "rtsp" || entry.Protocol == "rtsps" { - if ctx.Username != "" && ctx.Password != "" { - addURL(b.BuildURL(entry, altCtx)) - } - altCtx.Username = "" - altCtx.Password = "" - addURL(b.BuildURL(entry, altCtx)) - } else { - addURL(b.BuildURL(entry, altCtx)) - } - } - } b.logger.Debug("BuildURLsFromEntry complete", "entry_url_pattern", entry.URL,