Discovery of IGD vaguely in place. Start of parsing device schema.
This commit is contained in:
commit
fca075a6d5
23
LICENSE
Normal file
23
LICENSE
Normal 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
113
goupnp.go
Normal 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
69
udproundtripper.go
Normal 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
59
xml.go
Normal 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"`
|
||||
}
|
Loading…
Reference in New Issue
Block a user