352 lines
9.0 KiB
Go
352 lines
9.0 KiB
Go
package soap
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
originsoap "github.com/0x524a/onvif-go/internal/soap"
|
|
)
|
|
|
|
// Handler handles incoming SOAP requests
|
|
type Handler struct {
|
|
username string
|
|
password string
|
|
handlers map[string]MessageHandler
|
|
}
|
|
|
|
// MessageHandler is a function that handles a specific SOAP message
|
|
type MessageHandler func(body interface{}) (interface{}, error)
|
|
|
|
// NewHandler creates a new SOAP handler
|
|
func NewHandler(username, password string) *Handler {
|
|
return &Handler{
|
|
username: username,
|
|
password: password,
|
|
handlers: make(map[string]MessageHandler),
|
|
}
|
|
}
|
|
|
|
// RegisterHandler registers a handler for a specific action/message type
|
|
func (h *Handler) RegisterHandler(action string, handler MessageHandler) {
|
|
h.handlers[action] = handler
|
|
}
|
|
|
|
// ServeHTTP implements http.Handler interface
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
// Only accept POST requests
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Read request body
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
h.sendFault(w, "Receiver", "Failed to read request body", err.Error())
|
|
return
|
|
}
|
|
_ = r.Body.Close()
|
|
|
|
// Extract action from raw XML first (before parsing)
|
|
action := h.extractAction(body)
|
|
if action == "" {
|
|
h.sendFault(w, "Sender", "Unknown action", "Could not determine request action")
|
|
return
|
|
}
|
|
|
|
// Parse SOAP envelope
|
|
var envelope originsoap.Envelope
|
|
if err := xml.Unmarshal(body, &envelope); err != nil {
|
|
h.sendFault(w, "Sender", "Invalid SOAP envelope", err.Error())
|
|
return
|
|
}
|
|
|
|
// Authenticate if credentials are configured
|
|
if h.username != "" && h.password != "" {
|
|
if !h.authenticate(&envelope) {
|
|
h.sendFault(w, "Sender", "Authentication failed", "Invalid username or password")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Find and execute handler
|
|
handler, ok := h.handlers[action]
|
|
if !ok {
|
|
h.sendFault(w, "Receiver", "Action not supported", fmt.Sprintf("No handler for action: %s", action))
|
|
return
|
|
}
|
|
|
|
// Execute handler
|
|
response, err := handler(envelope.Body.Content)
|
|
if err != nil {
|
|
h.sendFault(w, "Receiver", "Handler error", err.Error())
|
|
return
|
|
}
|
|
|
|
// Send response
|
|
h.sendResponse(w, response)
|
|
}
|
|
|
|
// authenticate verifies the WS-Security credentials
|
|
func (h *Handler) authenticate(envelope *originsoap.Envelope) bool {
|
|
if envelope.Header == nil || envelope.Header.Security == nil || envelope.Header.Security.UsernameToken == nil {
|
|
return false
|
|
}
|
|
|
|
token := envelope.Header.Security.UsernameToken
|
|
|
|
// Check username
|
|
if token.Username != h.username {
|
|
return false
|
|
}
|
|
|
|
// Decode nonce
|
|
nonce, err := base64.StdEncoding.DecodeString(token.Nonce.Nonce)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Calculate expected digest
|
|
hash := sha1.New()
|
|
hash.Write(nonce)
|
|
hash.Write([]byte(token.Created))
|
|
hash.Write([]byte(h.password))
|
|
expectedDigest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
|
|
// Compare digests
|
|
return token.Password.Password == expectedDigest
|
|
}
|
|
|
|
// extractAction extracts the action/message type from the SOAP body
|
|
func (h *Handler) extractAction(bodyXML []byte) string {
|
|
// Parse XML to find the first element inside the Body element
|
|
decoder := xml.NewDecoder(bytes.NewReader(bodyXML))
|
|
inBody := false
|
|
depth := 0
|
|
|
|
for {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
switch t := token.(type) {
|
|
case xml.StartElement:
|
|
depth++
|
|
// Check if we're entering the Body element
|
|
if t.Name.Local == "Body" {
|
|
inBody = true
|
|
} else if inBody && depth > 2 {
|
|
// Found the first element inside Body
|
|
return t.Name.Local
|
|
}
|
|
case xml.EndElement:
|
|
depth--
|
|
if t.Name.Local == "Body" {
|
|
inBody = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// sendResponse sends a SOAP response
|
|
func (h *Handler) sendResponse(w http.ResponseWriter, response interface{}) {
|
|
envelope := &originsoap.Envelope{
|
|
Body: originsoap.Body{
|
|
Content: response,
|
|
},
|
|
}
|
|
|
|
// Marshal to XML
|
|
body, err := xml.MarshalIndent(envelope, "", " ")
|
|
if err != nil {
|
|
h.sendFault(w, "Receiver", "Failed to marshal response", err.Error())
|
|
return
|
|
}
|
|
|
|
// Add XML declaration
|
|
xmlBody := append([]byte(xml.Header), body...)
|
|
|
|
// Send response
|
|
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write(xmlBody)
|
|
}
|
|
|
|
// sendFault sends a SOAP fault response
|
|
func (h *Handler) sendFault(w http.ResponseWriter, code, reason, detail string) {
|
|
fault := &originsoap.Fault{
|
|
Code: code,
|
|
Reason: reason,
|
|
Detail: detail,
|
|
}
|
|
|
|
envelope := &originsoap.Envelope{
|
|
Body: originsoap.Body{
|
|
Fault: fault,
|
|
},
|
|
}
|
|
|
|
// Marshal to XML
|
|
body, err := xml.MarshalIndent(envelope, "", " ")
|
|
if err != nil {
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Add XML declaration
|
|
xmlBody := append([]byte(xml.Header), body...)
|
|
|
|
// Send fault response
|
|
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write(xmlBody)
|
|
}
|
|
|
|
// RequestWrapper wraps incoming SOAP request structures
|
|
type RequestWrapper struct {
|
|
XMLName xml.Name
|
|
Content []byte `xml:",innerxml"`
|
|
}
|
|
|
|
// ParseRequest parses a SOAP request into a specific structure
|
|
func ParseRequest(bodyContent interface{}, target interface{}) error {
|
|
// Marshal the body content back to XML
|
|
bodyXML, err := xml.Marshal(bodyContent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal body content: %w", err)
|
|
}
|
|
|
|
// Unmarshal into target structure
|
|
if err := xml.Unmarshal(bodyXML, target); err != nil {
|
|
return fmt.Errorf("failed to unmarshal request: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Common SOAP request/response structures for ONVIF
|
|
|
|
// GetSystemDateAndTimeRequest represents GetSystemDateAndTime request
|
|
type GetSystemDateAndTimeRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetSystemDateAndTime"`
|
|
}
|
|
|
|
// GetSystemDateAndTimeResponse represents GetSystemDateAndTime response
|
|
type GetSystemDateAndTimeResponse struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetSystemDateAndTimeResponse"`
|
|
SystemDateAndTime SystemDateAndTime `xml:"SystemDateAndTime"`
|
|
}
|
|
|
|
// SystemDateAndTime represents system date and time
|
|
type SystemDateAndTime struct {
|
|
DateTimeType string `xml:"DateTimeType"`
|
|
DaylightSavings bool `xml:"DaylightSavings"`
|
|
TimeZone TimeZone `xml:"TimeZone,omitempty"`
|
|
UTCDateTime DateTime `xml:"UTCDateTime,omitempty"`
|
|
LocalDateTime DateTime `xml:"LocalDateTime,omitempty"`
|
|
}
|
|
|
|
// TimeZone represents timezone information
|
|
type TimeZone struct {
|
|
TZ string `xml:"TZ"`
|
|
}
|
|
|
|
// DateTime represents date and time
|
|
type DateTime struct {
|
|
Time Time `xml:"Time"`
|
|
Date Date `xml:"Date"`
|
|
}
|
|
|
|
// Time represents time components
|
|
type Time struct {
|
|
Hour int `xml:"Hour"`
|
|
Minute int `xml:"Minute"`
|
|
Second int `xml:"Second"`
|
|
}
|
|
|
|
// Date represents date components
|
|
type Date struct {
|
|
Year int `xml:"Year"`
|
|
Month int `xml:"Month"`
|
|
Day int `xml:"Day"`
|
|
}
|
|
|
|
// ToDateTime converts time.Time to DateTime structure
|
|
func ToDateTime(t time.Time) DateTime {
|
|
return DateTime{
|
|
Date: Date{
|
|
Year: t.Year(),
|
|
Month: int(t.Month()),
|
|
Day: t.Day(),
|
|
},
|
|
Time: Time{
|
|
Hour: t.Hour(),
|
|
Minute: t.Minute(),
|
|
Second: t.Second(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// GetCapabilitiesRequest represents GetCapabilities request
|
|
type GetCapabilitiesRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetCapabilities"`
|
|
Category []string `xml:"Category,omitempty"`
|
|
}
|
|
|
|
// GetDeviceInformationRequest represents GetDeviceInformation request
|
|
type GetDeviceInformationRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetDeviceInformation"`
|
|
}
|
|
|
|
// GetServicesRequest represents GetServices request
|
|
type GetServicesRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/device/wsdl GetServices"`
|
|
IncludeCapability bool `xml:"IncludeCapability"`
|
|
}
|
|
|
|
// GetProfilesRequest represents GetProfiles request
|
|
type GetProfilesRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetProfiles"`
|
|
}
|
|
|
|
// GetStreamURIRequest represents GetStreamURI request
|
|
type GetStreamURIRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetStreamURI"`
|
|
StreamSetup StreamSetup `xml:"StreamSetup"`
|
|
ProfileToken string `xml:"ProfileToken"`
|
|
}
|
|
|
|
// StreamSetup represents stream setup parameters
|
|
type StreamSetup struct {
|
|
Stream string `xml:"Stream"`
|
|
Transport Transport `xml:"Transport"`
|
|
}
|
|
|
|
// Transport represents transport parameters
|
|
type Transport struct {
|
|
Protocol string `xml:"Protocol"`
|
|
}
|
|
|
|
// GetSnapshotURIRequest represents GetSnapshotURI request
|
|
type GetSnapshotURIRequest struct {
|
|
XMLName xml.Name `xml:"http://www.onvif.org/ver10/media/wsdl GetSnapshotURI"`
|
|
ProfileToken string `xml:"ProfileToken"`
|
|
}
|
|
|
|
// NormalizeAction normalizes SOAP action names
|
|
func NormalizeAction(action string) string {
|
|
// Remove namespace prefixes
|
|
if idx := strings.LastIndex(action, ":"); idx != -1 {
|
|
action = action[idx+1:]
|
|
}
|
|
return action
|
|
}
|