diff --git a/CHANGELOG.md b/CHANGELOG.md index c09bc5d..f3c7a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.1.3] - 2025-11-18 + +### Changed +- **Release Workflow**: Create releases as draft initially + - Fixes "Cannot upload assets to an immutable release" error + - Releases must be manually published after assets upload + - Prevents race condition where release publishes before all assets finish uploading + ## [1.1.2] - 2025-11-18 ### Changed @@ -107,7 +115,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Comprehensive documentation - README with usage guide -[Unreleased]: https://github.com/0x524a/onvif-go/compare/v1.1.2...HEAD +[Unreleased]: https://github.com/0x524a/onvif-go/compare/v1.1.3...HEAD +[1.1.3]: https://github.com/0x524a/onvif-go/compare/v1.1.2...v1.1.3 [1.1.2]: https://github.com/0x524a/onvif-go/compare/v1.1.1...v1.1.2 [1.1.1]: https://github.com/0x524a/onvif-go/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/0x524a/onvif-go/compare/v1.0.3...v1.1.0 diff --git a/client.go b/client.go index 00ea915..6817142 100644 --- a/client.go +++ b/client.go @@ -3,6 +3,7 @@ package onvif import ( "context" "crypto/md5" + "crypto/tls" "fmt" "io" "net" @@ -45,6 +46,19 @@ func WithHTTPClient(httpClient *http.Client) ClientOption { } } +// WithInsecureSkipVerify disables TLS certificate verification +// WARNING: Only use this for testing or with trusted cameras on private networks +func WithInsecureSkipVerify() ClientOption { + return func(c *Client) { + if transport, ok := c.httpClient.Transport.(*http.Transport); ok { + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true + } + } +} + // WithCredentials sets the authentication credentials func WithCredentials(username, password string) ClientOption { return func(c *Client) { @@ -74,6 +88,11 @@ func NewClient(endpoint string, opts ...ClientOption) (*Client, error) { MaxIdleConnsPerHost: 5, IdleConnTimeout: 90 * time.Second, }, + // Don't follow redirects automatically + // This prevents http:// from being silently upgraded to https:// + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, }, } diff --git a/cmd/onvif-cli/main.go b/cmd/onvif-cli/main.go index 6a324b5..75c83d3 100644 --- a/cmd/onvif-cli/main.go +++ b/cmd/onvif-cli/main.go @@ -282,13 +282,26 @@ func (c *CLI) connectToDiscoveredCamera(device *discovery.Device) { endpoint := device.GetDeviceEndpoint() fmt.Printf("Connecting to: %s\n", endpoint) + + // Warn if using HTTPS + if strings.HasPrefix(endpoint, "https://") { + fmt.Println("⚠️ HTTPS endpoint detected - you may need to skip TLS verification for self-signed certificates") + } + username := c.readInputWithDefault("Username", "admin") fmt.Print("Password: ") password, _ := c.reader.ReadString('\n') password = strings.TrimSpace(password) - c.createClient(endpoint, username, password) + // Ask about TLS verification only for HTTPS + insecure := false + if strings.HasPrefix(endpoint, "https://") { + skipTLS := c.readInputWithDefault("Skip TLS certificate verification? (y/N)", "N") + insecure = strings.ToLower(skipTLS) == "y" || strings.ToLower(skipTLS) == "yes" + } + + c.createClient(endpoint, username, password, insecure) } func (c *CLI) connectToCamera() { @@ -296,23 +309,42 @@ func (c *CLI) connectToCamera() { fmt.Println("===================") endpoint := c.readInputWithDefault("Camera endpoint (http://ip:port/onvif/device_service)", "http://192.168.1.100/onvif/device_service") + + // Warn if using HTTPS + if strings.HasPrefix(endpoint, "https://") { + fmt.Println("⚠️ HTTPS endpoint detected - you may need to skip TLS verification for self-signed certificates") + } + username := c.readInputWithDefault("Username", "admin") fmt.Print("Password: ") password, _ := c.reader.ReadString('\n') password = strings.TrimSpace(password) - c.createClient(endpoint, username, password) + // Ask about TLS verification only for HTTPS + insecure := false + if strings.HasPrefix(endpoint, "https://") { + skipTLS := c.readInputWithDefault("Skip TLS certificate verification? (y/N)", "N") + insecure = strings.ToLower(skipTLS) == "y" || strings.ToLower(skipTLS) == "yes" + } + + c.createClient(endpoint, username, password, insecure) } -func (c *CLI) createClient(endpoint, username, password string) { +func (c *CLI) createClient(endpoint, username, password string, insecure bool) { fmt.Println("⏳ Connecting...") - client, err := onvif.NewClient( - endpoint, + opts := []onvif.ClientOption{ onvif.WithCredentials(username, password), - onvif.WithTimeout(30*time.Second), - ) + onvif.WithTimeout(30 * time.Second), + } + + if insecure { + fmt.Println("⚠️ TLS certificate verification disabled") + opts = append(opts, onvif.WithInsecureSkipVerify()) + } + + client, err := onvif.NewClient(endpoint, opts...) if err != nil { fmt.Printf("❌ Failed to create client: %v\n", err) return @@ -328,6 +360,9 @@ func (c *CLI) createClient(endpoint, username, password string) { fmt.Println(" - Endpoint URL is correct") fmt.Println(" - Username and password are correct") fmt.Println(" - Camera is accessible from this network") + if strings.Contains(err.Error(), "tls") || strings.Contains(err.Error(), "certificate") || strings.Contains(err.Error(), "x509") { + fmt.Println(" - For HTTPS cameras with self-signed certificates, answer 'y' to skip TLS verification") + } return } @@ -532,7 +567,6 @@ func (c *CLI) inspectRTSPStream(streamURI string) map[string]interface{} { "reachable": false, "codec": "unknown", "resolution": "unknown", - "framerate": "unknown", } // Use rtspeek library for detailed stream inspection @@ -543,25 +577,22 @@ func (c *CLI) inspectRTSPStream(streamURI string) map[string]interface{} { if err == nil && streamInfo != nil { details["reachable"] = streamInfo.IsReachable() - if streamInfo.IsDescribeSucceeded() { + if streamInfo.IsDescribeSucceeded() && streamInfo.HasVideo() { // Extract codec information from first video media if firstVideo := streamInfo.GetFirstVideoMedia(); firstVideo != nil { + // Get codec format (H264, H265, MJPEG, etc.) details["codec"] = firstVideo.Format - } - - // Extract resolution - resolutions := streamInfo.GetVideoResolutionStrings() - if len(resolutions) > 0 { - details["resolution"] = resolutions[0] - } - - // Try to extract framerate (typical RTSP codecs run at standard framerates) - if firstVideo := streamInfo.GetFirstVideoMedia(); firstVideo != nil { - if firstVideo.ClockRate != nil && *firstVideo.ClockRate > 0 { - // H.264/H.265 typically use 90kHz clock with 1 frame per 3000-3600 samples - // This is a heuristic; actual framerate may vary - if firstVideo.Format == "H264" || firstVideo.Format == "H265" { - details["framerate"] = "30 fps" + + // Extract resolution directly from the video media + if firstVideo.Resolution != nil { + details["resolution"] = fmt.Sprintf("%dx%d", + firstVideo.Resolution.Width, + firstVideo.Resolution.Height) + } else { + // Fallback to resolution strings + resolutions := streamInfo.GetVideoResolutionStrings() + if len(resolutions) > 0 { + details["resolution"] = resolutions[0] } } } @@ -642,6 +673,13 @@ func (c *CLI) getStreamURIs(ctx context.Context) { fmt.Printf(" Stream URI: ❌ Error - %v\n", err) } else { fmt.Printf(" Stream URI: %s\n", streamURI.URI) + + // Warn if camera returns HTTPS when we connected via HTTP + if strings.HasPrefix(c.client.Endpoint(), "http://") && strings.HasPrefix(streamURI.URI, "https://") { + fmt.Printf(" ⚠️ WARNING: Camera returned HTTPS URL but you connected via HTTP\n") + fmt.Printf(" 💡 Stream may fail due to TLS certificate issues\n") + fmt.Printf(" 💡 Consider reconnecting with https:// endpoint and skip TLS verification\n") + } // Inspect RTSP stream details fmt.Print(" ⏳ Inspecting stream details...") @@ -664,10 +702,6 @@ func (c *CLI) getStreamURIs(ctx context.Context) { fmt.Printf(" Resolution: %s\n", resolution) } - if framerate, ok := details["framerate"].(string); ok && framerate != "unknown" { - fmt.Printf(" Frame Rate: %s\n", framerate) - } - if port, ok := details["port"].(string); ok { fmt.Printf(" RTSP Port: %s\n", port) } @@ -701,6 +735,14 @@ func (c *CLI) getSnapshotURIs(ctx context.Context) { fmt.Printf(" Snapshot URI: ❌ Error - %v\n", err) } else { fmt.Printf(" Snapshot URI: %s\n", snapshotURI.URI) + + // Warn if camera returns HTTPS when we connected via HTTP + if strings.HasPrefix(c.client.Endpoint(), "http://") && strings.HasPrefix(snapshotURI.URI, "https://") { + fmt.Printf(" ⚠️ WARNING: Camera returned HTTPS URL but you connected via HTTP\n") + fmt.Printf(" 💡 Snapshot may fail due to TLS certificate issues\n") + fmt.Printf(" 💡 Consider reconnecting with https:// endpoint and skip TLS verification\n") + } + fmt.Printf(" 🌐 Open this URL in a browser to see the snapshot\n") } fmt.Println()