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
import (
"encoding/xml"
"fmt"
"log"
"net/http"
"net/url"
)
// Non-exhaustive set of UPnP service types.
const (
// Search Target for InternetGatewayDevice.
SearchTargetIGD = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
ServiceTypeLayer3Forwarding = "urn:schemas-upnp-org:service:Layer3Forwarding: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.
//
// TODO: Fix implementation to discover multiple. Currently it will find a
// maximum of one.
func DiscoverIGD() ([]*IGD, error) {
// Non-exhaustive set of UPnP device types.
const (
// Device type for InternetGatewayDevice.
// http://upnp.org/specs/gw/upnp-gw-internetgatewaydevice-v1-device.pdf
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()
if err != nil {
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))
for _, response := range responses {
results := make([]MaybeRootDevice, len(responses))
for i, response := range responses {
maybe := &results[i]
loc, err := response.Location()
if err != nil {
log.Printf("goupnp: unexpected bad location from search: %v", err)
maybe.Err = ContextError{"unexpected bad location from search", err}
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 {
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
}
results = append(results, igd)
root.SetURLBase(urlBase)
maybe.Root = root
}
return results, nil
}
// IGD defines the interface for an Internet Gateway Device.
type IGD struct {
xml xmlRootDevice
}
func requestIgd(serviceUrl string) (*IGD, error) {
resp, err := http.Get(serviceUrl)
func requestXml(url string, defaultSpace string, doc interface{}) error {
resp, err := http.Get(url)
if err != nil {
return nil, err
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("goupnp: got response status %s from IGD at %q",
resp.Status, serviceUrl)
return fmt.Errorf("goupnp: got response status %s from %q",
resp.Status, url)
}
decoder := xml.NewDecoder(resp.Body)
decoder.DefaultSpace = deviceXmlNs
var xml xmlRootDevice
if err = decoder.Decode(&xml); err != nil {
return nil, err
}
log.Printf("%+v", xml)
decoder.DefaultSpace = defaultSpace
return &IGD{xml}, nil
}
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)
return decoder.Decode(doc)
}

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",
n, len(requestBuf.Bytes()))
}
time.Sleep(10 * time.Millisecond)
time.Sleep(5 * time.Millisecond)
}
// Await responses until timeout.

57
xml.go
View File

@ -4,25 +4,35 @@ package goupnp
import (
"encoding/xml"
"fmt"
"net/url"
)
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`
SpecVersion xmlSpecVersion `xml:"specVersion"`
URLBase string `xml:"URLBase"`
Device xmlDevice `xml:"device"`
SpecVersion SpecVersion `xml:"specVersion"`
URLBaseStr string `xml:"URLBase"`
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"`
Minor int32 `xml:"minor"`
}
type xmlDevice struct {
type Device struct {
URLBase *url.URL `xml:"-"`
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"`
@ -34,15 +44,29 @@ type xmlDevice struct {
SerialNumber string `xml:"serialNumber"`
UDN string `xml:"UDN"`
UPC string `xml:"UPC,omitempty"`
Icons []xmlIcon `xml:"iconList>icon,omitempty"`
Services []xmlService `xml:"serviceList>service,omitempty"`
Devices []xmlDevice `xml:"deviceList>device,omitempty"`
Icons []*Icon `xml:"iconList>icon,omitempty"`
Services []*Service `xml:"serviceList>service,omitempty"`
Devices []*Device `xml:"deviceList>device,omitempty"`
// Extra observed elements:
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"`
Width int32 `xml:"width"`
Height int32 `xml:"height"`
@ -50,10 +74,19 @@ type xmlIcon struct {
URL string `xml:"url"`
}
type xmlService struct {
type Service struct {
URLBase *url.URL `xml:"-"`
ServiceType string `xml:"serviceType"`
ServiceId string `xml:"serviceId"`
SCPDURL string `xml:"SCPDURL"`
ControlURL string `xml:"controlURL"`
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)
}