Discovery of IGD vaguely in place. Start of parsing device schema.

This commit is contained in:
John Beisley 2013-09-26 23:11:06 +01:00
commit fca075a6d5
4 changed files with 264 additions and 0 deletions

23
LICENSE Normal file
View File

@ -0,0 +1,23 @@
Copyright (c) 2013, John Beisley <greatred@gmail.com>
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.

113
goupnp.go Normal file
View File

@ -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)
}

69
udproundtripper.go Normal file
View File

@ -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
}

59
xml.go Normal file
View File

@ -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"`
}