Files
onvif-go/soap/soap.go
T
2025-10-30 00:50:27 +00:00

221 lines
5.9 KiB
Go

package soap
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"net/http"
"time"
)
// 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
type Header struct {
Security *Security `xml:"Security,omitempty"`
}
// Body represents a SOAP body
type Body struct {
Content interface{} `xml:",omitempty"`
Fault *Fault `xml:"Fault,omitempty"`
}
// 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"`
Reason string `xml:"Reason>Text"`
Detail string `xml:"Detail,omitempty"`
}
// 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"`
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
type UsernameToken struct {
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
type Password struct {
Type string `xml:"Type,attr"`
Password string `xml:",chardata"`
}
// Nonce represents a WS-Security nonce
type Nonce struct {
Type string `xml:"EncodingType,attr"`
Nonce string `xml:",chardata"`
}
// Client represents a SOAP client
type Client struct {
httpClient *http.Client
username string
password string
}
// NewClient creates a new SOAP client
func NewClient(httpClient *http.Client, username, password string) *Client {
return &Client{
httpClient: httpClient,
username: username,
password: password,
}
}
// 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 {
// Build SOAP envelope
envelope := &Envelope{
Body: Body{
Content: request,
},
}
// Add security header if credentials are provided
if c.username != "" && c.password != "" {
envelope.Header = &Header{
Security: c.createSecurityHeader(),
}
}
// Marshal envelope to XML
body, err := xml.MarshalIndent(envelope, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal SOAP envelope: %w", err)
}
// Add XML declaration
xmlBody := append([]byte(xml.Header), body...)
// Create HTTP request
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(xmlBody))
if err != nil {
return fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/soap+xml; charset=utf-8")
if action != "" {
req.Header.Set("SOAPAction", action)
}
// Send request
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send HTTP request: %w", err)
}
defer resp.Body.Close()
// Read response body
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
// Check HTTP status
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("HTTP request failed with status %d: %s", resp.StatusCode, string(respBody))
}
// Parse response
var respEnvelope Envelope
if err := xml.Unmarshal(respBody, &respEnvelope); err != nil {
return fmt.Errorf("failed to unmarshal SOAP response: %w", err)
}
// Check for SOAP fault
if respEnvelope.Body.Fault != nil {
return fmt.Errorf("SOAP fault: [%s] %s - %s",
respEnvelope.Body.Fault.Code,
respEnvelope.Body.Fault.Reason,
respEnvelope.Body.Fault.Detail)
}
// Unmarshal response content
if response != nil {
// Re-marshal the body content and unmarshal into the response struct
bodyXML, err := xml.Marshal(respEnvelope.Body.Content)
if err != nil {
return fmt.Errorf("failed to marshal response body: %w", err)
}
if err := xml.Unmarshal(bodyXML, response); err != nil {
return fmt.Errorf("failed to unmarshal response: %w", err)
}
}
return nil
}
// createSecurityHeader creates a WS-Security header with username token digest
func (c *Client) createSecurityHeader() *Security {
// Generate nonce
nonceBytes := make([]byte, 16)
rand.Read(nonceBytes)
nonce := base64.StdEncoding.EncodeToString(nonceBytes)
// Get current timestamp
created := time.Now().UTC().Format(time.RFC3339)
// Calculate password digest: Base64(SHA1(nonce + created + password))
hash := sha1.New()
hash.Write(nonceBytes)
hash.Write([]byte(created))
hash.Write([]byte(c.password))
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
return &Security{
MustUnderstand: "1",
UsernameToken: &UsernameToken{
Username: c.username,
Password: Password{
Type: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest",
Password: digest,
},
Nonce: Nonce{
Type: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary",
Nonce: nonce,
},
Created: created,
},
}
}
// BuildEnvelope builds a SOAP envelope with the given body content
func BuildEnvelope(body interface{}, username, password string) (*Envelope, error) {
envelope := &Envelope{
Body: Body{
Content: body,
},
}
if username != "" && password != "" {
client := &Client{username: username, password: password}
envelope.Header = &Header{
Security: client.createSecurityHeader(),
}
}
return envelope, nil
}