feat: add comprehensive ONVIF test reports and enhance documentation

- Introduced CAMERA_TEST_REPORT.md and COMPREHENSIVE_TEST_SUMMARY.md to document testing results for the Bosch FLEXIDOME indoor 5100i IR camera.
- Added detailed analysis of ONVIF Media Service operations and implementation status in MEDIA_OPERATIONS_ANALYSIS.md and MEDIA_WSDL_OPERATIONS_ANALYSIS.md.
- Updated implementation status documentation to reflect the completion of all 79 operations in the ONVIF Media Service.
- Enhanced existing comments and documentation across various files for better clarity and consistency.
This commit is contained in:
0x524a
2025-12-02 02:29:51 -05:00
parent e530575bc1
commit 9e3b5e0170
61 changed files with 3001 additions and 1070 deletions
+11
View File
@@ -0,0 +1,11 @@
package soap
import "errors"
var (
// ErrHTTPRequestFailed is returned when an HTTP request fails.
ErrHTTPRequestFailed = errors.New("HTTP request failed")
// ErrEmptyResponseBody is returned when a response body is empty.
ErrEmptyResponseBody = errors.New("received empty response body")
)
+30 -25
View File
@@ -1,3 +1,4 @@
// Package soap provides SOAP client functionality for ONVIF communication.
package soap
import (
@@ -13,25 +14,25 @@ import (
"time"
)
// Envelope represents a SOAP envelope
// Envelope represents a SOAP envelope.
type Envelope struct {
XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Envelope"`
Header *Header `xml:"http://www.w3.org/2003/05/soap-envelope Header,omitempty"`
Body Body `xml:"http://www.w3.org/2003/05/soap-envelope Body"`
}
// Header represents a SOAP header
// Header represents a SOAP header.
type Header struct {
Security *Security `xml:"Security,omitempty"`
}
// Body represents a SOAP body
// Body represents a SOAP body.
type Body struct {
Content interface{} `xml:",omitempty"`
Fault *Fault `xml:"Fault,omitempty"`
}
// Fault represents a SOAP fault
// Fault represents a SOAP fault.
type Fault struct {
XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Fault"`
Code string `xml:"Code>Value"`
@@ -39,35 +40,35 @@ type Fault struct {
Detail string `xml:"Detail,omitempty"`
}
// Security represents WS-Security header
// Security represents WS-Security header.
type Security struct {
XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd Security"`
XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd Security"`
MustUnderstand string `xml:"http://www.w3.org/2003/05/soap-envelope mustUnderstand,attr,omitempty"`
UsernameToken *UsernameToken `xml:"UsernameToken,omitempty"`
}
// UsernameToken represents a WS-Security username token
// UsernameToken represents a WS-Security username token.
type UsernameToken struct {
XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd UsernameToken"`
XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd UsernameToken"`
Username string `xml:"Username"`
Password Password `xml:"Password"`
Nonce Nonce `xml:"Nonce"`
Created string `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd Created"`
}
// Password represents a WS-Security password
// Password represents a WS-Security password.
type Password struct {
Type string `xml:"Type,attr"`
Password string `xml:",chardata"`
}
// Nonce represents a WS-Security nonce
// Nonce represents a WS-Security nonce.
type Nonce struct {
Type string `xml:"EncodingType,attr"`
Nonce string `xml:",chardata"`
}
// Client represents a SOAP client
// Client represents a SOAP client.
type Client struct {
httpClient *http.Client
username string
@@ -76,7 +77,7 @@ type Client struct {
logger func(format string, args ...interface{})
}
// NewClient creates a new SOAP client
// NewClient creates a new SOAP client.
func NewClient(httpClient *http.Client, username, password string) *Client {
return &Client{
httpClient: httpClient,
@@ -87,21 +88,21 @@ func NewClient(httpClient *http.Client, username, password string) *Client {
}
}
// SetDebug enables debug logging with a custom logger
// SetDebug enables debug logging with a custom logger.
func (c *Client) SetDebug(enabled bool, logger func(format string, args ...interface{})) {
c.debug = enabled
c.logger = logger
}
// logDebug logs debug information if debug mode is enabled
func (c *Client) logDebug(format string, args ...interface{}) {
// logDebugf logs debug information if debug mode is enabled.
func (c *Client) logDebugf(format string, args ...interface{}) {
if c.debug && c.logger != nil {
c.logger(format, args...)
}
}
// Call makes a SOAP call to the specified endpoint
func (c *Client) Call(ctx context.Context, endpoint string, action string, request interface{}, response interface{}) error {
// Call makes a SOAP call to the specified endpoint.
func (c *Client) Call(ctx context.Context, endpoint, action string, request, response interface{}) error {
// Build SOAP envelope
envelope := &Envelope{
Body: Body{
@@ -126,7 +127,7 @@ func (c *Client) Call(ctx context.Context, endpoint string, action string, reque
xmlBody := append([]byte(xml.Header), body...)
// Log request if debug is enabled
c.logDebug("=== SOAP Request ===\nEndpoint: %s\nAction: %s\n%s\n", endpoint, action, string(xmlBody))
c.logDebugf("=== SOAP Request ===\nEndpoint: %s\nAction: %s\n%s\n", endpoint, action, string(xmlBody))
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(xmlBody))
@@ -145,7 +146,10 @@ func (c *Client) Call(ctx context.Context, endpoint string, action string, reque
if err != nil {
return fmt.Errorf("failed to send HTTP request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
defer func() {
//nolint:errcheck // Close error is not critical for cleanup
_ = resp.Body.Close()
}()
// Read response body
respBody, err := io.ReadAll(resp.Body)
@@ -154,16 +158,16 @@ func (c *Client) Call(ctx context.Context, endpoint string, action string, reque
}
// Log response if debug is enabled
c.logDebug("=== SOAP Response ===\nStatus: %d\n%s\n", resp.StatusCode, string(respBody))
c.logDebugf("=== SOAP Response ===\nStatus: %d\n%s\n", resp.StatusCode, string(respBody))
// Check HTTP status
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP request failed with status %d: %s", resp.StatusCode, string(respBody))
return fmt.Errorf("%w with status %d: %s", ErrHTTPRequestFailed, resp.StatusCode, string(respBody))
}
// If response is empty, return immediately
if len(respBody) == 0 {
return fmt.Errorf("received empty response body")
return fmt.Errorf("%w", ErrEmptyResponseBody)
}
// Unmarshal response content if response is provided
@@ -188,11 +192,12 @@ func (c *Client) Call(ctx context.Context, endpoint string, action string, reque
return nil
}
// createSecurityHeader creates a WS-Security header with username token digest
// createSecurityHeader creates a WS-Security header with username token digest.
func (c *Client) createSecurityHeader() *Security {
// Generate nonce
nonceBytes := make([]byte, 16)
_, _ = rand.Read(nonceBytes) // rand.Read always returns len(nonceBytes), nil
//nolint:errcheck // rand.Read always returns len(nonceBytes), nil for sufficient entropy
_, _ = rand.Read(nonceBytes)
nonce := base64.StdEncoding.EncodeToString(nonceBytes)
// Get current timestamp
@@ -222,7 +227,7 @@ func (c *Client) createSecurityHeader() *Security {
}
}
// BuildEnvelope builds a SOAP envelope with the given body content
// BuildEnvelope builds a SOAP envelope with the given body content.
func BuildEnvelope(body interface{}, username, password string) (*Envelope, error) {
envelope := &Envelope{
Body: Body{
+7
View File
@@ -84,6 +84,7 @@ func TestBuildEnvelope(t *testing.T) {
if (err != nil) != tt.wantErr {
t.Errorf("BuildEnvelope() error = %v, wantErr %v", err, tt.wantErr)
return
}
@@ -114,6 +115,8 @@ func TestClientCall(t *testing.T) {
{
name: "successful request",
setupServer: func(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/soap+xml")
w.WriteHeader(http.StatusOK)
@@ -135,6 +138,8 @@ func TestClientCall(t *testing.T) {
{
name: "unauthorized request",
setupServer: func(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
@@ -146,6 +151,8 @@ func TestClientCall(t *testing.T) {
{
name: "http error status",
setupServer: func(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("Internal Server Error"))