Keep things generic for the time being. Refactor for simplicity.
This commit is contained in:
parent
9bddaed1e9
commit
26ceb1478f
144
goupnp.go
144
goupnp.go
@ -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)
|
|
||||||
}
|
}
|
||||||
|
2
httpu.go
2
httpu.go
@ -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.
|
||||||
|
91
xml.go
91
xml.go
@ -4,45 +4,69 @@ 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 {
|
||||||
Name xml.Name `xml:"root`
|
URLBase *url.URL `xml:"-"`
|
||||||
SpecVersion xmlSpecVersion `xml:"specVersion"`
|
Name xml.Name `xml:"root`
|
||||||
URLBase string `xml:"URLBase"`
|
SpecVersion SpecVersion `xml:"specVersion"`
|
||||||
Device xmlDevice `xml:"device"`
|
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"`
|
Major int32 `xml:"major"`
|
||||||
Minor int32 `xml:"minor"`
|
Minor int32 `xml:"minor"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type xmlDevice struct {
|
type Device struct {
|
||||||
DeviceType string `xml:"deviceType"`
|
URLBase *url.URL `xml:"-"`
|
||||||
FriendlyName string `xml:"friendlyName"`
|
DeviceType string `xml:"deviceType"`
|
||||||
Manufacturer string `xml:"manufacturer"`
|
FriendlyName string `xml:"friendlyName"`
|
||||||
ManufacturerURL string `xml:"manufacturerURL"`
|
Manufacturer string `xml:"manufacturer"`
|
||||||
ModelDescription string `xml:"modelDescription"`
|
ManufacturerURL string `xml:"manufacturerURL"`
|
||||||
ModelName string `xml:"modelName"`
|
ModelDescription string `xml:"modelDescription"`
|
||||||
ModelNumber string `xml:"modelNumber"`
|
ModelName string `xml:"modelName"`
|
||||||
ModelURL string `xml:"modelURL"`
|
ModelNumber string `xml:"modelNumber"`
|
||||||
SerialNumber string `xml:"serialNumber"`
|
ModelURL string `xml:"modelURL"`
|
||||||
UDN string `xml:"UDN"`
|
SerialNumber string `xml:"serialNumber"`
|
||||||
UPC string `xml:"UPC,omitempty"`
|
UDN string `xml:"UDN"`
|
||||||
Icons []xmlIcon `xml:"iconList>icon,omitempty"`
|
UPC string `xml:"UPC,omitempty"`
|
||||||
Services []xmlService `xml:"serviceList>service,omitempty"`
|
Icons []*Icon `xml:"iconList>icon,omitempty"`
|
||||||
Devices []xmlDevice `xml:"deviceList>device,omitempty"`
|
Services []*Service `xml:"serviceList>service,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 {
|
||||||
ServiceType string `xml:"serviceType"`
|
URLBase *url.URL `xml:"-"`
|
||||||
ServiceId string `xml:"serviceId"`
|
ServiceType string `xml:"serviceType"`
|
||||||
SCPDURL string `xml:"SCPDURL"`
|
ServiceId string `xml:"serviceId"`
|
||||||
ControlURL string `xml:"controlURL"`
|
SCPDURL string `xml:"SCPDURL"`
|
||||||
EventSubURL string `xml:"eventSubURL"`
|
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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user