From fca075a6d5bc77776fe86a731fc7cf61a4d168c1 Mon Sep 17 00:00:00 2001 From: John Beisley Date: Thu, 26 Sep 2013 23:11:06 +0100 Subject: [PATCH] Discovery of IGD vaguely in place. Start of parsing device schema. --- LICENSE | 23 +++++++++ goupnp.go | 113 +++++++++++++++++++++++++++++++++++++++++++++ udproundtripper.go | 69 +++++++++++++++++++++++++++ xml.go | 59 +++++++++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 LICENSE create mode 100644 goupnp.go create mode 100644 udproundtripper.go create mode 100644 xml.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..252e3d6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2013, John Beisley +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/goupnp.go b/goupnp.go new file mode 100644 index 0000000..6bdc9ec --- /dev/null +++ b/goupnp.go @@ -0,0 +1,113 @@ +package goupnp + +import ( + "encoding/xml" + "errors" + "fmt" + "log" + "io" + "os" + "net/http" + "net/url" +) + +const ( + ssdpUDP4Addr = "239.255.255.250:1900" + + methodSearch = "M-SEARCH" + // Search Target for InternetGatewayDevice. + stIgd = "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + hdrMan = `"ssdp:discover"` +) + +// 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) { + hc := http.Client{ + Transport: udpRoundTripper{}, + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return errors.New("goupnp: unexpected HTTP redirect") + }, + Jar: nil, + } + + request := http.Request{ + Method: methodSearch, + // TODO: Support both IPv4 and IPv6. + Host: ssdpUDP4Addr, + URL: &url.URL{Opaque: "*"}, + Header: http.Header{ + // Putting headers in here avoids them being title-cased. + // (The UPnP discovery protocol uses case-sensitive headers) + "HOST": []string{ssdpUDP4Addr}, + "MX": []string{"2"}, // TODO: Variable max wait time. + "MAN": []string{hdrMan}, + "ST": []string{stIgd}, + }, + } + + response, err := hc.Do(&request) + if err != nil { + return nil, err + } + + // Any errors past this point are simply "no result found". We log the + // errors, but report no results. In a future version of this implementation, + // multiple *good* results can be returned. + + if response.StatusCode != 200 { + log.Printf("goupnp: response code %d %q from UPnP discovery", + response.StatusCode, response.Status) + return nil, nil + } + if st := response.Header.Get("ST"); st != stIgd { + log.Printf("goupnp: got unexpected search target result %q", st) + return nil, nil + } + + location, err := response.Location() + if err != nil { + log.Printf("goupnp: missing location in response") + return nil, nil + } + + igd, err := requestIgd(location.String()) + if err != nil { + log.Printf("goupnp: error requesting IGD: %v", err) + return nil, nil + } + + return []IGD{igd}, nil +} + +// IGD defines the interface for an Internet Gateway Device. +type IGD interface { +} + +type igd struct { + serviceUrl string +} + +func requestIgd(serviceUrl string) (IGD, error) { + resp, err := http.Get(serviceUrl) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + decoder := xml.NewDecoder(io.TeeReader(resp.Body, os.Stdout)) + decoder.DefaultSpace = deviceXmlNs + var root xmlRootDevice + if err = decoder.Decode(&root); err != nil { + return nil, err + } + log.Printf("%+v", root) + + return igd{serviceUrl}, nil +} + +func (device *igd) String() string { + return fmt.Sprintf("goupnp.IGD @ %s", device.serviceUrl) +} diff --git a/udproundtripper.go b/udproundtripper.go new file mode 100644 index 0000000..ceeeaa8 --- /dev/null +++ b/udproundtripper.go @@ -0,0 +1,69 @@ +package goupnp + +import ( + "bufio" + "bytes" + "fmt" + "net" + "net/http" + "time" +) + +// TODO: RoundTripper is probably the wrong interface, as there could be +// multiple responses to a request. + +type udpRoundTripper struct { + // If zero, defaults to 3 second deadline (a zero deadline makes no sense). + Deadline time.Duration + MaxWaitSeconds int +} + +func (urt udpRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + conn, err := net.ListenPacket("udp", ":0") + if err != nil { + return nil, err + } + defer conn.Close() + + var requestBuf bytes.Buffer + if err := r.Write(&requestBuf); err != nil { + return nil, err + } + destAddr, err := net.ResolveUDPAddr("udp", r.Host) + if err != nil { + return nil, err + } + + deadline := urt.Deadline + if urt.Deadline == 0 { + deadline = 3 * time.Second + } + + if err = conn.SetDeadline(time.Now().Add(deadline)); err != nil { + return nil, err + } + + // Send request. + if n, err := conn.WriteTo(requestBuf.Bytes(), destAddr); err != nil { + return nil, err + } else if n < len(requestBuf.Bytes()) { + return nil, fmt.Errorf("goupnp: wrote %d bytes rather than full %d in request", + n, len(requestBuf.Bytes())) + } + + // Await response. + responseBytes := make([]byte, 2048) + n, _, err := conn.ReadFrom(responseBytes) + if err != nil { + return nil, err + } + responseBytes = responseBytes[:n] + + // Parse response. + response, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(responseBytes)), r) + if err != nil { + return nil, err + } + + return response, err +} diff --git a/xml.go b/xml.go new file mode 100644 index 0000000..5300a78 --- /dev/null +++ b/xml.go @@ -0,0 +1,59 @@ +// This file contains XML structures for communicating with UPnP devices. + +package goupnp + +import ( + "encoding/xml" +) + +const ( + deviceXmlNs = "urn:schemas-upnp-org:device-1-0" +) + +type xmlRootDevice struct { + Name xml.Name `xml:"root` + SpecVersion xmlSpecVersion `xml:"specVersion"` + URLBase string `xml:"URLBase"` + Device xmlDevice `xml:"device"` +} + +type xmlSpecVersion struct { + Major int32 `xml:"major"` + Minor int32 `xml:"minor"` +} + +type xmlDevice struct { + DeviceType string `xml:"deviceType"` + FriendlyName string `xml:"friendlyName"` + Manufacturer string `xml:"manufacturer"` + ManufacturerURL string `xml:"manufacturerURL"` + ModelDescription string `xml:"modelDescription"` + ModelName string `xml:"modelName"` + ModelNumber string `xml:"modelNumber"` + ModelURL string `xml:"modelURL"` + 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"` + + // Extra observed elements: + PresentationURL string `xml:"presentationURL"` +} + +type xmlIcon struct { + Mimetype string `xml:"mimetype"` + Width int32 `xml:"width"` + Height int32 `xml:"height"` + Depth int32 `xml:"depth"` + URL string `xml:"url"` +} + +type xmlService struct { + ServiceType string `xml:"serviceType"` + ServiceId string `xml:"serviceId"` + SCPDURL string `xml:"SCPDURL"` + ControlURL string `xml:"controlURL"` + EventSubURL string `xml:"eventSubURL"` +}