diff --git a/cmd/discoverigd/discoverigd.go b/cmd/discoverigd/discoverigd.go index 6a69448..33c446e 100644 --- a/cmd/discoverigd/discoverigd.go +++ b/cmd/discoverigd/discoverigd.go @@ -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) } } } diff --git a/soap.go b/soap.go index 668f51f..efa87aa 100644 --- a/soap.go +++ b/soap.go @@ -12,46 +12,29 @@ import ( ) const ( - SoapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" + soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" + soapPrefix = `` + soapSuffix = `` ) -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"` -}