From a5c769dd6c58e7d68f1b3ca822cc3483638b800e Mon Sep 17 00:00:00 2001 From: eduard256 Date: Wed, 29 Oct 2025 02:31:58 +0300 Subject: [PATCH] Add credential embedding in stream URLs for SSE events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embed username:password@ credentials in discovered stream URLs for basic_auth and combined authentication methods, making URLs directly usable by clients without additional auth handling. **Changes:** - Add embedCredentialsInURL() function in scanner.go - Embed credentials for basic_auth and combined methods only - Skip embedding for no_auth, query_params, digest, url_embedded - Handle edge cases: empty credentials, existing credentials in URL - Apply in scanDirectStream() and testURLsConcurrently() **Security:** - URL encoding handled by url.UserPassword() - Checks prevent credential duplication - Only processes when username AND password are provided **Results:** Before: http://10.0.20.112/snapshot.jpg After: http://admin:password@10.0.20.112/snapshot.jpg Tested with Zosi ZG23213M camera - all 6 streams working correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/camera/discovery/scanner.go | 51 ++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/internal/camera/discovery/scanner.go b/internal/camera/discovery/scanner.go index b52e5bb..c367814 100644 --- a/internal/camera/discovery/scanner.go +++ b/internal/camera/discovery/scanner.go @@ -178,8 +178,12 @@ func (s *Scanner) scanDirectStream(ctx context.Context, req models.StreamDiscove if testResult.Working { result.TotalFound = 1 + + // Embed credentials in URL for basic_auth and combined methods + finalURL := s.embedCredentialsInURL(testResult.URL, req.Username, req.Password, string(testResult.AuthMethod)) + discoveredStream := models.DiscoveredStream{ - URL: testResult.URL, + URL: finalURL, Type: testResult.Type, Protocol: testResult.Protocol, Working: true, @@ -231,6 +235,46 @@ func (s *Scanner) extractIP(target string) string { return target } +// embedCredentialsInURL embeds username and password in URL for basic_auth and combined methods +func (s *Scanner) embedCredentialsInURL(streamURL, username, password, authMethod string) string { + // Only apply for basic_auth and combined methods + if authMethod != "basic_auth" && authMethod != "combined" { + return streamURL + } + + // Check if credentials are provided + if username == "" || password == "" { + return streamURL + } + + // Parse URL + u, err := url.Parse(streamURL) + if err != nil { + s.logger.Debug("failed to parse URL for credential embedding", + "url", streamURL, + "error", err.Error()) + return streamURL + } + + // Check if credentials already exist in URL + if u.User != nil { + s.logger.Debug("credentials already exist in URL, skipping embedding", + "url", streamURL) + return streamURL + } + + // Embed credentials + u.User = url.UserPassword(username, password) + embeddedURL := u.String() + + s.logger.Debug("credentials embedded in URL", + "original_url", streamURL, + "embedded_url", embeddedURL, + "auth_method", authMethod) + + return embeddedURL +} + // collectURLs collects all URLs to test func (s *Scanner) collectURLs(ctx context.Context, req models.StreamDiscoveryRequest, ip string) ([]string, error) { var allURLs []string @@ -444,8 +488,11 @@ func (s *Scanner) testURLsConcurrently(ctx context.Context, urls []string, req m if testResult.Working { atomic.AddInt32(&found, 1) + // Embed credentials in URL for basic_auth and combined methods + finalURL := s.embedCredentialsInURL(testResult.URL, req.Username, req.Password, string(testResult.AuthMethod)) + discoveredStream := models.DiscoveredStream{ - URL: testResult.URL, + URL: finalURL, Type: testResult.Type, Protocol: testResult.Protocol, Port: 0, // Will be extracted from URL if needed