Keep things generic for the time being. Refactor for simplicity.

This commit is contained in:
John Beisley 2013-09-28 17:57:51 +01:00
parent 9bddaed1e9
commit 26ceb1478f
3 changed files with 123 additions and 114 deletions

144
goupnp.go
View File

@ -1,122 +1,98 @@
// goupnp is an implementation of a client for UPnP devices.
package goupnp package goupnp
import ( import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"log"
"net/http" "net/http"
"net/url"
) )
// Non-exhaustive set of UPnP service types.
const ( const (
// Search Target for InternetGatewayDevice. ServiceTypeLayer3Forwarding = "urn:schemas-upnp-org:service:Layer3Forwarding:1"
SearchTargetIGD = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" ServiceTypeWANCommonInterfaceConfig = "WANCommonInterfaceConfig:1"
// WANPPPConnection is typically useful with regard to the external IP and
// port forwarding.
ServiceTypeWANPPPConnection = "WANPPPConnection:1"
) )
// DiscoverIGD attempts to find Internet Gateway Devices. // Non-exhaustive set of UPnP device types.
// const (
// TODO: Fix implementation to discover multiple. Currently it will find a // Device type for InternetGatewayDevice.
// maximum of one. // http://upnp.org/specs/gw/upnp-gw-internetgatewaydevice-v1-device.pdf
func DiscoverIGD() ([]*IGD, error) { DeviceTypeInternetGatewayDevice = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
)
type ContextError struct {
Context string
Err error
}
func (err ContextError) Error() string {
return fmt.Sprintf("%s: %v", err.Context, err.Err)
}
type MaybeRootDevice struct {
Root *RootDevice
Err error
}
// DiscoverDevices attempts to find targets of the given type. searchTarget is
// typically a value from a DeviceType* constant. An error is returned for
// errors while attempting to send the query. An error or RootDevice is
// returned for each discovered service.
func DiscoverDevices(searchTarget string) ([]MaybeRootDevice, error) {
httpu, err := NewHTTPUClient() httpu, err := NewHTTPUClient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
responses, err := SSDPRawSearch(httpu, SearchTargetIGD, 2, 3) defer httpu.Close()
responses, err := SSDPRawSearch(httpu, string(searchTarget), 2, 3)
if err != nil {
return nil, err
}
results := make([]*IGD, 0, len(responses)) results := make([]MaybeRootDevice, len(responses))
for _, response := range responses { for i, response := range responses {
maybe := &results[i]
loc, err := response.Location() loc, err := response.Location()
if err != nil { if err != nil {
log.Printf("goupnp: unexpected bad location from search: %v", err) maybe.Err = ContextError{"unexpected bad location from search", err}
continue continue
} }
igd, err := requestIgd(loc.String()) locStr := loc.String()
root := new(RootDevice)
if err := requestXml(locStr, DeviceXMLNamespace, root); err != nil {
maybe.Err = ContextError{fmt.Sprintf("error requesting root device details from %q", locStr), err}
continue
}
urlBase, err := url.Parse(root.URLBaseStr)
if err != nil { if err != nil {
log.Printf("goupnp: error requesting IGD: %v", err) maybe.Err = ContextError{fmt.Sprintf("error parsing URLBase %q from %q: %v", root.URLBaseStr, locStr), err}
continue continue
} }
results = append(results, igd) root.SetURLBase(urlBase)
maybe.Root = root
} }
return results, nil return results, nil
} }
// IGD defines the interface for an Internet Gateway Device. func requestXml(url string, defaultSpace string, doc interface{}) error {
type IGD struct { resp, err := http.Get(url)
xml xmlRootDevice
}
func requestIgd(serviceUrl string) (*IGD, error) {
resp, err := http.Get(serviceUrl)
if err != nil { if err != nil {
return nil, err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return nil, fmt.Errorf("goupnp: got response status %s from IGD at %q", return fmt.Errorf("goupnp: got response status %s from %q",
resp.Status, serviceUrl) resp.Status, url)
} }
decoder := xml.NewDecoder(resp.Body) decoder := xml.NewDecoder(resp.Body)
decoder.DefaultSpace = deviceXmlNs decoder.DefaultSpace = defaultSpace
var xml xmlRootDevice
if err = decoder.Decode(&xml); err != nil {
return nil, err
}
log.Printf("%+v", xml)
return &IGD{xml}, nil return decoder.Decode(doc)
}
func (igd *IGD) Device() *Device {
return &Device{
igd.xml.URLBase,
igd.xml.Device,
}
}
func (igd *IGD) String() string {
return fmt.Sprintf("IGD{UDN: %q friendlyName: %q}",
igd.xml.Device.UDN, igd.xml.Device.FriendlyName)
}
type Device struct {
urlBase string
xml xmlDevice
}
func (device *Device) String() string {
return fmt.Sprintf("Device{friendlyName: %q}", device.xml.FriendlyName)
}
func (device *Device) Devices() []*Device {
devices := make([]*Device, len(device.xml.Devices))
for i, childXml := range device.xml.Devices {
devices[i] = &Device{
device.urlBase,
childXml,
}
}
return devices
}
func (device *Device) Services() []*Service {
srvs := make([]*Service, len(device.xml.Services))
for i, childXml := range device.xml.Services {
srvs[i] = &Service{
device.urlBase,
childXml,
}
}
return srvs
}
type Service struct {
urlBase string
xml xmlService
}
func (srv *Service) String() string {
return fmt.Sprintf("Service{serviceType: %q}", srv.xml.ServiceType)
} }

View File

@ -67,7 +67,7 @@ func (httpu *HTTPUClient) Do(req *http.Request, timeout time.Duration, numSends
return nil, fmt.Errorf("httpu: wrote %d bytes rather than full %d in request", return nil, fmt.Errorf("httpu: wrote %d bytes rather than full %d in request",
n, len(requestBuf.Bytes())) n, len(requestBuf.Bytes()))
} }
time.Sleep(10 * time.Millisecond) time.Sleep(5 * time.Millisecond)
} }
// Await responses until timeout. // Await responses until timeout.

57
xml.go
View File

@ -4,25 +4,35 @@ package goupnp
import ( import (
"encoding/xml" "encoding/xml"
"fmt"
"net/url"
) )
const ( const (
deviceXmlNs = "urn:schemas-upnp-org:device-1-0" DeviceXMLNamespace = "urn:schemas-upnp-org:device-1-0"
) )
type xmlRootDevice struct { type RootDevice struct {
URLBase *url.URL `xml:"-"`
Name xml.Name `xml:"root` Name xml.Name `xml:"root`
SpecVersion xmlSpecVersion `xml:"specVersion"` SpecVersion SpecVersion `xml:"specVersion"`
URLBase string `xml:"URLBase"` URLBaseStr string `xml:"URLBase"`
Device xmlDevice `xml:"device"` Device *Device `xml:"device"`
} }
type xmlSpecVersion struct { func (root *RootDevice) SetURLBase(urlBase *url.URL) {
root.URLBase = urlBase
root.URLBaseStr = urlBase.String()
root.Device.SetURLBase(urlBase)
}
type SpecVersion struct {
Major int32 `xml:"major"` Major int32 `xml:"major"`
Minor int32 `xml:"minor"` Minor int32 `xml:"minor"`
} }
type xmlDevice struct { type Device struct {
URLBase *url.URL `xml:"-"`
DeviceType string `xml:"deviceType"` DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"` FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"` Manufacturer string `xml:"manufacturer"`
@ -34,15 +44,29 @@ type xmlDevice struct {
SerialNumber string `xml:"serialNumber"` SerialNumber string `xml:"serialNumber"`
UDN string `xml:"UDN"` UDN string `xml:"UDN"`
UPC string `xml:"UPC,omitempty"` UPC string `xml:"UPC,omitempty"`
Icons []xmlIcon `xml:"iconList>icon,omitempty"` Icons []*Icon `xml:"iconList>icon,omitempty"`
Services []xmlService `xml:"serviceList>service,omitempty"` Services []*Service `xml:"serviceList>service,omitempty"`
Devices []xmlDevice `xml:"deviceList>device,omitempty"` Devices []*Device `xml:"deviceList>device,omitempty"`
// Extra observed elements: // Extra observed elements:
PresentationURL string `xml:"presentationURL"` PresentationURL string `xml:"presentationURL"`
} }
type xmlIcon struct { func (device *Device) SetURLBase(urlBase *url.URL) {
device.URLBase = urlBase
for _, srv := range device.Services {
srv.SetURLBase(urlBase)
}
for _, child := range device.Devices {
child.SetURLBase(urlBase)
}
}
func (device *Device) String() string {
return fmt.Sprintf("Device ID %s : %s (%s)", device.UDN, device.DeviceType, device.FriendlyName)
}
type Icon struct {
Mimetype string `xml:"mimetype"` Mimetype string `xml:"mimetype"`
Width int32 `xml:"width"` Width int32 `xml:"width"`
Height int32 `xml:"height"` Height int32 `xml:"height"`
@ -50,10 +74,19 @@ type xmlIcon struct {
URL string `xml:"url"` URL string `xml:"url"`
} }
type xmlService struct { type Service struct {
URLBase *url.URL `xml:"-"`
ServiceType string `xml:"serviceType"` ServiceType string `xml:"serviceType"`
ServiceId string `xml:"serviceId"` ServiceId string `xml:"serviceId"`
SCPDURL string `xml:"SCPDURL"` SCPDURL string `xml:"SCPDURL"`
ControlURL string `xml:"controlURL"` ControlURL string `xml:"controlURL"`
EventSubURL string `xml:"eventSubURL"` EventSubURL string `xml:"eventSubURL"`
} }
func (srv *Service) SetURLBase(urlBase *url.URL) {
srv.URLBase = urlBase
}
func (srv *Service) String() string {
return fmt.Sprintf("Service ID %s : %s", srv.ServiceId, srv.ServiceType)
}