221 lines
5.9 KiB
Go
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
|
|
}
|