Add or update .codecov copy.yml

This commit is contained in:
ProtoTess
2026-01-16 04:11:59 +00:00
parent ef340c0e5a
commit 66f6a4e838
391 changed files with 131885 additions and 0 deletions
+368
View File
@@ -0,0 +1,368 @@
// Package soap provides SOAP request handling for the ONVIF server.
package soap
import (
"bytes"
"crypto/sha1" //nolint:gosec // SHA1 used for ONVIF digest authentication
"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() //nolint:gosec // SHA1 required for ONVIF digest auth
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)
//nolint:errcheck // Write error is not critical after WriteHeader
_, _ = 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 - use appropriate status code based on fault code
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
statusCode := http.StatusInternalServerError
if code == "Sender" {
statusCode = http.StatusBadRequest
}
w.WriteHeader(statusCode)
//nolint:errcheck // Write error is not critical after WriteHeader
_, _ = 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, 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
}
+442
View File
@@ -0,0 +1,442 @@
package soap
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"encoding/xml"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
const testXMLHeader = `<?xml version="1.0"?>`
func TestNewHandler(t *testing.T) {
handler := NewHandler("admin", "password")
if handler == nil {
t.Error("NewHandler returned nil")
return
}
if handler.username != "admin" {
t.Errorf("Username mismatch: got %s, want admin", handler.username)
}
if handler.password != "password" {
t.Errorf("Password mismatch: got %s, want password", handler.password)
}
if handler.handlers == nil {
t.Error("Handlers map is nil")
}
}
func TestRegisterHandler(t *testing.T) {
handler := NewHandler("admin", "password")
testHandler := func(body interface{}) (interface{}, error) {
return "test response", nil
}
handler.RegisterHandler("TestAction", testHandler)
if _, ok := handler.handlers["TestAction"]; !ok {
t.Error("Handler not registered")
}
}
func TestServeHTTPMethodNotAllowed(t *testing.T) {
handler := NewHandler("admin", "password")
req := httptest.NewRequest("GET", "/", http.NoBody)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("Expected status %d, got %d", http.StatusMethodNotAllowed, w.Code)
}
}
func TestServeHTTPValidSOAPRequest(t *testing.T) {
handler := NewHandler("", "") // No authentication
// Create test handler
handler.RegisterHandler("TestAction", func(body interface{}) (interface{}, error) {
return map[string]string{"Result": "Success"}, nil
})
// Create SOAP request
soapBody := testXMLHeader + `
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<TestAction/>
</soap:Body>
</soap:Envelope>`
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code == http.StatusInternalServerError {
t.Errorf("Handler returned error: %s", w.Body.String())
}
}
func TestServeHTTPInvalidSOAPEnvelope(t *testing.T) {
handler := NewHandler("", "")
invalidXML := `<?xml version="1.0"?>
<invalid>
<xml>not soap</xml>
</invalid>`
req := httptest.NewRequest("POST", "/", strings.NewReader(invalidXML))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Should return a SOAP fault
if !strings.Contains(w.Body.String(), "Fault") {
t.Errorf("Expected SOAP fault, got: %s", w.Body.String())
}
}
func TestServeHTTPUnknownAction(t *testing.T) {
handler := NewHandler("", "")
soapBody := `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<UnknownAction/>
</soap:Body>
</soap:Envelope>`
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if !strings.Contains(w.Body.String(), "Fault") {
t.Errorf("Expected SOAP fault for unknown action")
}
}
func TestExtractAction(t *testing.T) {
handler := NewHandler("", "")
tests := []struct {
name string
soapBody string
expectedAction string
}{
{
name: "Simple action",
soapBody: `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetDeviceInformation/>
</soap:Body>
</soap:Envelope>`,
expectedAction: "GetDeviceInformation",
},
{
name: "Action with namespace",
soapBody: `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<tds:GetDeviceInformation xmlns:tds="http://www.onvif.org/ver10/device/wsdl"/>
</soap:Body>
</soap:Envelope>`,
expectedAction: "GetDeviceInformation",
},
{
name: "Action with attributes",
soapBody: `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<GetProfiles>
<param>value</param>
</GetProfiles>
</soap:Body>
</soap:Envelope>`,
expectedAction: "GetProfiles",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
action := handler.extractAction([]byte(tt.soapBody))
if action != tt.expectedAction {
t.Errorf("Expected action %s, got %s", tt.expectedAction, action)
}
})
}
}
func TestExtractActionInvalid(t *testing.T) {
handler := NewHandler("", "")
invalidXML := "not valid xml at all"
action := handler.extractAction([]byte(invalidXML))
if action != "" {
t.Errorf("Expected empty action for invalid XML, got %s", action)
}
}
func TestSendFault(t *testing.T) {
handler := NewHandler("", "")
w := httptest.NewRecorder()
handler.sendFault(w, "Sender", "Test error", "Test error message")
if w.Code != http.StatusBadRequest {
t.Errorf("Expected status 400, got %d", w.Code)
}
response := w.Body.String()
if !strings.Contains(response, "Fault") {
t.Error("Response should contain Fault element")
}
if !strings.Contains(response, "Test error") {
t.Error("Response should contain error message")
}
}
func TestSendResponse(t *testing.T) {
handler := NewHandler("", "")
w := httptest.NewRecorder()
response := map[string]string{
"Result": "Success",
}
handler.sendResponse(w, response)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
body := w.Body.String()
if body == "" {
t.Error("Response body is empty")
}
}
func TestAuthenticate(t *testing.T) {
handler := NewHandler("admin", "password")
// Create a proper WS-Security header
nonce := "test_nonce_12345"
created := "2024-01-01T00:00:00Z"
// Calculate digest
hash := sha1.New()
hash.Write([]byte(nonce))
hash.Write([]byte(created))
hash.Write([]byte("password"))
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
soapBody := `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<soap:Header>
<wsse:Security>
<wsse:UsernameToken>
<wsse:Username>admin</wsse:Username>
<wsse:Password>` + digest + `</wsse:Password>
<wsse:Nonce>` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
<wsse:Created>` + created + `</wsse:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<TestAction/>
</soap:Body>
</soap:Envelope>`
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
w := httptest.NewRecorder()
handler.RegisterHandler("TestAction", func(body interface{}) (interface{}, error) {
return "authenticated", nil
})
handler.ServeHTTP(w, req)
// Should succeed or indicate authentication was checked
if w.Code == http.StatusInternalServerError && strings.Contains(w.Body.String(), "Authentication") {
t.Logf("Authentication check passed (expected behavior)")
}
}
func TestAuthenticateFailsWithWrongPassword(t *testing.T) {
handler := NewHandler("admin", "correct_password")
// Calculate digest with wrong password
nonce := "test_nonce_12345"
created := "2024-01-01T00:00:00Z"
hash := sha1.New()
hash.Write([]byte(nonce))
hash.Write([]byte(created))
hash.Write([]byte("wrong_password")) // Wrong password
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
soapBody := `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<soap:Header>
<wsse:Security>
<wsse:UsernameToken>
<wsse:Username>admin</wsse:Username>
<wsse:Password>` + digest + `</wsse:Password>
<wsse:Nonce>` + base64.StdEncoding.EncodeToString([]byte(nonce)) + `</wsse:Nonce>
<wsse:Created>` + created + `</wsse:Created>
</wsse:UsernameToken>
</wsse:Security>
</soap:Header>
<soap:Body>
<TestAction/>
</soap:Body>
</soap:Envelope>`
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
w := httptest.NewRecorder()
handler.RegisterHandler("TestAction", func(body interface{}) (interface{}, error) {
return "should not reach here", nil
})
handler.ServeHTTP(w, req)
// Should fail authentication
if !strings.Contains(w.Body.String(), "Fault") {
t.Errorf("Expected authentication failure")
}
}
func TestHandlerWithoutAuthentication(t *testing.T) {
handler := NewHandler("", "") // No authentication
soapBody := testXMLHeader + `
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<TestAction/>
</soap:Body>
</soap:Envelope>`
handler.RegisterHandler("TestAction", func(body interface{}) (interface{}, error) {
return "success", nil
})
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Should succeed without authentication
if w.Code == http.StatusInternalServerError && strings.Contains(w.Body.String(), "Authentication") {
t.Errorf("Should not require authentication when not configured")
}
}
func TestReadRequestBodyError(t *testing.T) {
handler := NewHandler("", "")
// Create a request with a body that will fail to read
req := httptest.NewRequest("POST", "/", &failingReader{})
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if !strings.Contains(w.Body.String(), "Fault") {
t.Errorf("Expected SOAP fault for read error")
}
}
// Helper types and functions
type failingReader struct{}
func (f *failingReader) Read(p []byte) (n int, err error) {
return 0, io.ErrUnexpectedEOF
}
func TestResponseHandling(t *testing.T) {
handler := NewHandler("", "")
type TestResponse struct {
XMLName xml.Name `xml:"TestActionResponse"`
Result string `xml:"Result"`
}
handler.RegisterHandler("TestAction", func(body interface{}) (interface{}, error) {
return &TestResponse{Result: "Success"}, nil
})
soapBody := `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<TestAction/>
</soap:Body>
</soap:Envelope>`
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
response := w.Body.String()
if !strings.Contains(response, "TestActionResponse") {
t.Errorf("Response should contain TestActionResponse element")
}
}
func TestEmptyBody(t *testing.T) {
handler := NewHandler("", "")
req := httptest.NewRequest("POST", "/", bytes.NewReader([]byte("")))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
if !strings.Contains(w.Body.String(), "Fault") {
t.Errorf("Expected SOAP fault for empty body")
}
}
func TestContentType(t *testing.T) {
handler := NewHandler("", "")
handler.RegisterHandler("TestAction", func(body interface{}) (interface{}, error) {
return "test", nil
})
soapBody := `<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<TestAction/>
</soap:Body>
</soap:Envelope>`
req := httptest.NewRequest("POST", "/", strings.NewReader(soapBody))
req.Header.Set("Content-Type", "application/soap+xml")
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// Handler should work regardless of content type
if w.Code == http.StatusInternalServerError {
t.Logf("Note: Handler may validate content type")
}
}