Tidy up the SOAP components a bit.
This commit is contained in:
		@@ -3,6 +3,7 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/huin/goupnp"
 | 
			
		||||
@@ -10,6 +11,36 @@ import (
 | 
			
		||||
 | 
			
		||||
var spaces = "                                     "
 | 
			
		||||
 | 
			
		||||
type GetExternalIPAddressRequest struct {
 | 
			
		||||
	XMLName xml.Name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetExternalIPAddressResponse struct {
 | 
			
		||||
	XMLName              xml.Name
 | 
			
		||||
	NewExternalIPAddress string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Uint16Value struct {
 | 
			
		||||
	XMLName xml.Name
 | 
			
		||||
	Value   uint16 `xml:",chardata"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetGenericPortMappingEntryRequest struct {
 | 
			
		||||
	XMLName             xml.Name
 | 
			
		||||
	NewPortMappingIndex Uint16Value
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GetGenericPortMappingEntryResponse struct {
 | 
			
		||||
	XMLName           xml.Name
 | 
			
		||||
	NewRemoteHost     string
 | 
			
		||||
	NewExternalPort   uint16
 | 
			
		||||
	NewProtocol       string
 | 
			
		||||
	NewInternalPort   uint16
 | 
			
		||||
	NewInternalClient string
 | 
			
		||||
	NewEnabled        string // boolean
 | 
			
		||||
	NewLeaseDuration  uint32
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type indentLevel int
 | 
			
		||||
 | 
			
		||||
func (i indentLevel) String() string {
 | 
			
		||||
@@ -48,12 +79,39 @@ func main() {
 | 
			
		||||
				fmt.Printf("Got more than one expected service on device %s\n", device.FriendlyName)
 | 
			
		||||
			}
 | 
			
		||||
			srv := wanPPPSrvs[0]
 | 
			
		||||
			results, err := goupnp.PerformSoapAction(goupnp.ServiceTypeWANPPPConnection, "GetExternalIPAddress", &srv.ControlURL.URL, nil)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				fmt.Printf("Failed to GetExternalIPAddress from %s: %v\n", device.FriendlyName, err)
 | 
			
		||||
				continue
 | 
			
		||||
 | 
			
		||||
			if scdp, err := srv.RequestSCDP(); err != nil {
 | 
			
		||||
				fmt.Printf("Error requesting SCPD: %v\n", err)
 | 
			
		||||
			} else {
 | 
			
		||||
				fmt.Println("Available SCPD actions:")
 | 
			
		||||
				for _, action := range scdp.Actions {
 | 
			
		||||
					fmt.Println(" ", action.Name)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			srvClient := goupnp.NewSOAPClient(srv.ControlURL.URL)
 | 
			
		||||
 | 
			
		||||
			{
 | 
			
		||||
				inAction := GetExternalIPAddressRequest{XMLName: xml.Name{Space: goupnp.ServiceTypeWANPPPConnection, Local: "GetExternalIPAddress"}}
 | 
			
		||||
				var outAction GetExternalIPAddressResponse
 | 
			
		||||
				err := srvClient.PerformAction(goupnp.ServiceTypeWANPPPConnection, "GetExternalIPAddress", &inAction, &outAction)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					fmt.Printf("Failed to GetExternalIPAddress from %s: %v\n", device.FriendlyName, err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				fmt.Printf("Got GetExternalIPAddress result from %s: %+v\n", device.FriendlyName, outAction)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for i := uint16(0); i < 10; i++ {
 | 
			
		||||
				inAction := GetGenericPortMappingEntryRequest{XMLName: xml.Name{Space: goupnp.ServiceTypeWANPPPConnection, Local: "GetGenericPortMappingEntry"}, NewPortMappingIndex: Uint16Value{XMLName: xml.Name{"", "NewPortMappingIndex"}, Value: i}}
 | 
			
		||||
				var outAction GetGenericPortMappingEntryResponse
 | 
			
		||||
				err := srvClient.PerformAction(goupnp.ServiceTypeWANPPPConnection, "GetGenericPortMappingEntry", &inAction, &outAction)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					fmt.Printf("Failed to GetGenericPortMappingEntry on %s: %v\n", device.FriendlyName, err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				fmt.Printf("Got GetGenericPortMappingEntry from %s: %+v\n", device.FriendlyName, outAction)
 | 
			
		||||
			}
 | 
			
		||||
			fmt.Printf("Got GetExternalIPAddress result from %s: %v\n", device.FriendlyName, results)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										115
									
								
								soap.go
									
									
									
									
									
								
							
							
						
						
									
										115
									
								
								soap.go
									
									
									
									
									
								
							@@ -12,46 +12,29 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	SoapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
 | 
			
		||||
	soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
 | 
			
		||||
	soapPrefix        = `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`
 | 
			
		||||
	soapSuffix        = `</s:Body></s:Envelope>`
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type NameValue struct {
 | 
			
		||||
	Name  string
 | 
			
		||||
	Value string
 | 
			
		||||
type SOAPClient struct {
 | 
			
		||||
	EndpointURL url.URL
 | 
			
		||||
	HTTPClient  http.Client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewSoapAction creates a SoapEnvelope with the given action and arguments.
 | 
			
		||||
func newSoapAction(actionNamespace, actionName string, arguments []NameValue) *SoapEnvelope {
 | 
			
		||||
	env := &SoapEnvelope{
 | 
			
		||||
		EncodingStyle: SoapEncodingStyle,
 | 
			
		||||
		Body: SoapBody{
 | 
			
		||||
			Action: SoapAction{
 | 
			
		||||
				XMLName:   xml.Name{actionNamespace, actionName},
 | 
			
		||||
				Arguments: make([]SoapArgument, len(arguments)),
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
func NewSOAPClient(endpointURL url.URL) *SOAPClient {
 | 
			
		||||
	return &SOAPClient{
 | 
			
		||||
		EndpointURL: endpointURL,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range arguments {
 | 
			
		||||
		env.Body.Action.Arguments[i].XMLName.Local = arguments[i].Name
 | 
			
		||||
		env.Body.Action.Arguments[i].Value = arguments[i].Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return env
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PerformSoapAction makes a SOAP request, with the given action.
 | 
			
		||||
func PerformSoapAction(actionNamespace, actionName string, url *url.URL, arguments []NameValue) ([]NameValue, error) {
 | 
			
		||||
	requestEnv := newSoapAction(actionNamespace, actionName, arguments)
 | 
			
		||||
	requestBytes, err := xml.Marshal(requestEnv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
// PerformSOAPAction makes a SOAP request, with the given action.
 | 
			
		||||
func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAction interface{}, outAction interface{}) error {
 | 
			
		||||
	requestBytes, err := encodeRequestAction(inAction)
 | 
			
		||||
 | 
			
		||||
	client := http.Client{}
 | 
			
		||||
	response, err := client.Do(&http.Request{
 | 
			
		||||
	response, err := client.HTTPClient.Do(&http.Request{
 | 
			
		||||
		Method: "POST",
 | 
			
		||||
		URL:    url,
 | 
			
		||||
		URL:    &client.EndpointURL,
 | 
			
		||||
		Header: http.Header{
 | 
			
		||||
			"SOAPACTION":   []string{actionNamespace + "#" + actionName},
 | 
			
		||||
			"CONTENT-TYPE": []string{"text/xml; charset=\"utf-8\""},
 | 
			
		||||
@@ -61,61 +44,71 @@ func PerformSoapAction(actionNamespace, actionName string, url *url.URL, argumen
 | 
			
		||||
		ContentLength: int64(len(requestBytes)),
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer response.Body.Close()
 | 
			
		||||
	if response.StatusCode != 200 {
 | 
			
		||||
		return nil, fmt.Errorf("goupnp: SOAP request got %s response from %s", response.Status, url.String())
 | 
			
		||||
		return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	responseEnv := newSOAPEnvelope()
 | 
			
		||||
	decoder := xml.NewDecoder(response.Body)
 | 
			
		||||
	var responseEnv SoapEnvelope
 | 
			
		||||
	if err := decoder.Decode(&responseEnv); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	if err := decoder.Decode(responseEnv); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if responseEnv.Body.Fault != nil {
 | 
			
		||||
		return nil, responseEnv.Body.Fault
 | 
			
		||||
		return responseEnv.Body.Fault
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	results := make([]NameValue, len(responseEnv.Body.Action.Arguments))
 | 
			
		||||
	for i, soapArg := range responseEnv.Body.Action.Arguments {
 | 
			
		||||
		results[i] = NameValue{
 | 
			
		||||
			Name:  soapArg.XMLName.Local,
 | 
			
		||||
			Value: soapArg.Value,
 | 
			
		||||
		}
 | 
			
		||||
	if err := xml.Unmarshal(responseEnv.Body.RawAction, outAction); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return results, nil
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SoapEnvelope struct {
 | 
			
		||||
// newSOAPAction creates a soapEnvelope with the given action and arguments.
 | 
			
		||||
func newSOAPEnvelope() *soapEnvelope {
 | 
			
		||||
	return &soapEnvelope{
 | 
			
		||||
		EncodingStyle: soapEncodingStyle,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// encodeRequestAction is a hacky way to create an encoded SOAP envelope
 | 
			
		||||
// containing the given action. Experiments with one router have shown that it
 | 
			
		||||
// 500s for requests where the outer default xmlns is set to the SOAP
 | 
			
		||||
// namespace, and then reassigning the default namespace within that to the
 | 
			
		||||
// service namespace. Hand-coding the outer XML to work-around this.
 | 
			
		||||
func encodeRequestAction(inAction interface{}) ([]byte, error) {
 | 
			
		||||
	requestBuf := new(bytes.Buffer)
 | 
			
		||||
	requestBuf.WriteString(soapPrefix)
 | 
			
		||||
	requestEnc := xml.NewEncoder(requestBuf)
 | 
			
		||||
	if err := requestEnc.Encode(inAction); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	requestBuf.WriteString(soapSuffix)
 | 
			
		||||
	return requestBuf.Bytes(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type soapEnvelope struct {
 | 
			
		||||
	XMLName       xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
 | 
			
		||||
	EncodingStyle string   `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"`
 | 
			
		||||
	Body          SoapBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
 | 
			
		||||
	Body          soapBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SoapBody struct {
 | 
			
		||||
	Fault  *SoapFault `xml:"Fault"`
 | 
			
		||||
	Action SoapAction `xml:",any"`
 | 
			
		||||
type soapBody struct {
 | 
			
		||||
	Fault     *SOAPFaultError `xml:"Fault"`
 | 
			
		||||
	RawAction []byte          `xml:",innerxml"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SoapFault struct {
 | 
			
		||||
// SOAPFaultError implements error, and contains SOAP fault information.
 | 
			
		||||
type SOAPFaultError struct {
 | 
			
		||||
	FaultCode   string `xml:"faultcode"`
 | 
			
		||||
	FaultString string `xml:"faultstring"`
 | 
			
		||||
	Detail      string `xml:"detail"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (err *SoapFault) Error() string {
 | 
			
		||||
func (err *SOAPFaultError) Error() string {
 | 
			
		||||
	return fmt.Sprintf("SOAP fault: %s", err.FaultString)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SoapAction struct {
 | 
			
		||||
	XMLName   xml.Name
 | 
			
		||||
	Arguments []SoapArgument `xml:",any"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SoapArgument struct {
 | 
			
		||||
	XMLName xml.Name
 | 
			
		||||
	Value   string `xml:",chardata"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user