Make UPnP discovery requests on each interface explicitly (fixes #1113)

This commit is contained in:
Audrius Butkevicius 2015-04-16 10:26:09 +01:00
parent 2a31031cbc
commit 1b69c2441c

View File

@ -91,42 +91,71 @@ type upnpRoot struct {
} }
// Discover discovers UPnP InternetGatewayDevices. // Discover discovers UPnP InternetGatewayDevices.
// The order in which the devices appear in the result list is not deterministic. // The order in which the devices appear in the results list is not deterministic.
func Discover(timeout time.Duration) []IGD { func Discover(timeout time.Duration) []IGD {
var result []IGD var results []IGD
l.Infoln("Starting UPnP discovery...") l.Infoln("Starting UPnP discovery...")
// Search for InternetGatewayDevice:2 devices interfaces, err := net.Interfaces()
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:2", timeout, result)...) if err != nil {
l.Infoln("Listing network interfaces:", err)
return results
}
// Search for InternetGatewayDevice:1 devices resultChan := make(chan IGD, 16)
// InternetGatewayDevice:2 devices that correctly respond to the IGD:1 request as well will not be re-added to the result list
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:1", timeout, result)...)
if len(result) > 0 && debug { // Aggregator
l.Debugln("UPnP discovery result:") go func() {
for _, resultDevice := range result { next:
l.Debugln("[" + resultDevice.uuid + "]") for result := range resultChan {
for _, existingResult := range results {
for _, resultService := range resultDevice.services { if existingResult.uuid == result.uuid {
l.Debugln("* [" + resultService.serviceID + "] " + resultService.serviceURL) if debug {
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
for _, svc := range result.services {
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
}
}
goto next
}
}
results = append(results, result)
if debug {
l.Debugf("UPnP discovery result %s with services:", result.uuid)
for _, svc := range result.services {
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
} }
} }
} }
}()
var wg sync.WaitGroup
for _, intf := range interfaces {
for _, deviceType := range []string{"urn:schemas-upnp-org:device:InternetGatewayDevice:1", "urn:schemas-upnp-org:device:InternetGatewayDevice:2"} {
wg.Add(1)
go func(intf net.Interface, deviceType string) {
discover(&intf, deviceType, timeout, resultChan)
wg.Done()
}(intf, deviceType)
}
}
wg.Wait()
close(resultChan)
suffix := "devices" suffix := "devices"
if len(result) == 1 { if len(results) == 1 {
suffix = "device" suffix = "device"
} }
l.Infof("UPnP discovery complete (found %d %s).", len(result), suffix) l.Infof("UPnP discovery complete (found %d %s).", len(results), suffix)
return result return results
} }
// Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices. // Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices.
// The order in which the devices appear in the result list is not deterministic // The order in which the devices appear in the result list is not deterministic
func discover(deviceType string, timeout time.Duration, knownDevices []IGD) []IGD { func discover(intf *net.Interface, deviceType string, timeout time.Duration, results chan<- IGD) {
ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900} ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
tpl := `M-SEARCH * HTTP/1.1 tpl := `M-SEARCH * HTTP/1.1
@ -141,39 +170,34 @@ Mx: %d
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1)) search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
if debug { if debug {
l.Debugln("Starting discovery of device type " + deviceType + "...") l.Debugln("Starting discovery of device type " + deviceType + " on " + intf.Name)
} }
var results []IGD socket, err := net.ListenMulticastUDP("udp4", intf, &net.UDPAddr{IP: ssdp.IP})
resultChannel := make(chan IGD, 8)
socket, err := net.ListenMulticastUDP("udp4", nil, &net.UDPAddr{IP: ssdp.IP})
if err != nil { if err != nil {
l.Infoln(err) l.Infoln(err)
return results return
} }
defer socket.Close() // Make sure our socket gets closed defer socket.Close() // Make sure our socket gets closed
err = socket.SetDeadline(time.Now().Add(timeout)) err = socket.SetDeadline(time.Now().Add(timeout))
if err != nil { if err != nil {
l.Infoln(err) l.Infoln(err)
return results return
} }
if debug { if debug {
l.Debugln("Sending search request for device type " + deviceType + "...") l.Debugln("Sending search request for device type " + deviceType + " on " + intf.Name)
} }
var resultWaitGroup sync.WaitGroup
_, err = socket.WriteTo(search, ssdp) _, err = socket.WriteTo(search, ssdp)
if err != nil { if err != nil {
l.Infoln(err) l.Infoln(err)
return results return
} }
if debug { if debug {
l.Debugln("Listening for UPnP response for device type " + deviceType + "...") l.Debugln("Listening for UPnP response for device type " + deviceType + " on " + intf.Name)
} }
// Listen for responses until a timeout is reached // Listen for responses until a timeout is reached
@ -184,67 +208,40 @@ Mx: %d
if e, ok := err.(net.Error); !ok || !e.Timeout() { if e, ok := err.(net.Error); !ok || !e.Timeout() {
l.Infoln(err) //legitimate error, not a timeout. l.Infoln(err) //legitimate error, not a timeout.
} }
break break
} else {
// Process results in a separate go routine so we can immediately return to listening for more responses
resultWaitGroup.Add(1)
go handleSearchResponse(deviceType, knownDevices, resp, n, resultChannel, &resultWaitGroup)
}
}
// Wait for all result handlers to finish processing, then close result channel
resultWaitGroup.Wait()
close(resultChannel)
// Collect our results from the result handlers using the result channel
for result := range resultChannel {
// Check for existing results (some routers send multiple response packets)
for _, existingResult := range results {
if existingResult.uuid == result.uuid {
if debug {
l.Debugln("Already processed device with UUID", existingResult.uuid, "continuing...")
} }
igd, err := parseResponse(deviceType, resp[:n])
if err != nil {
l.Infoln(err)
continue continue
} }
results <- igd
} }
// No existing results, okay to append
results = append(results, result)
}
if debug { if debug {
l.Debugln("Discovery for device type " + deviceType + " finished.") l.Debugln("Discovery for device type " + deviceType + " on " + intf.Name + " finished.")
} }
return results
} }
func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, length int, resultChannel chan<- IGD, resultWaitGroup *sync.WaitGroup) { func parseResponse(deviceType string, resp []byte) (IGD, error) {
defer resultWaitGroup.Done() // Signal when we've finished processing
if debug { if debug {
l.Debugln("Handling UPnP response:\n\n" + string(resp[:length])) l.Debugln("Handling UPnP response:\n\n" + string(resp))
} }
reader := bufio.NewReader(bytes.NewBuffer(resp[:length])) reader := bufio.NewReader(bytes.NewBuffer(resp))
request := &http.Request{} request := &http.Request{}
response, err := http.ReadResponse(reader, request) response, err := http.ReadResponse(reader, request)
if err != nil { if err != nil {
l.Infoln(err) return IGD{}, err
return
} }
respondingDeviceType := response.Header.Get("St") respondingDeviceType := response.Header.Get("St")
if respondingDeviceType != deviceType { if respondingDeviceType != deviceType {
l.Infoln("Unrecognized UPnP device of type " + respondingDeviceType) return IGD{}, errors.New("unrecognized UPnP device of type " + respondingDeviceType)
return
} }
deviceDescriptionLocation := response.Header.Get("Location") deviceDescriptionLocation := response.Header.Get("Location")
if deviceDescriptionLocation == "" { if deviceDescriptionLocation == "" {
l.Infoln("Invalid IGD response: no location specified.") return IGD{}, errors.New("invalid IGD response: no location specified.")
return
} }
deviceDescriptionURL, err := url.Parse(deviceDescriptionLocation) deviceDescriptionURL, err := url.Parse(deviceDescriptionLocation)
@ -255,8 +252,7 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
deviceUSN := response.Header.Get("USN") deviceUSN := response.Header.Get("USN")
if deviceUSN == "" { if deviceUSN == "" {
l.Infoln("Invalid IGD response: USN not specified.") return IGD{}, errors.New("invalid IGD response: USN not specified.")
return
} }
deviceUUID := strings.TrimLeft(strings.Split(deviceUSN, "::")[0], "uuid:") deviceUUID := strings.TrimLeft(strings.Split(deviceUSN, "::")[0], "uuid:")
@ -265,39 +261,25 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
l.Infoln("Invalid IGD response: invalid device UUID", deviceUUID, "(continuing anyway)") l.Infoln("Invalid IGD response: invalid device UUID", deviceUUID, "(continuing anyway)")
} }
// Don't re-add devices that are already known
for _, knownDevice := range knownDevices {
if deviceUUID == knownDevice.uuid {
if debug {
l.Debugln("Ignoring known device with UUID " + deviceUUID)
}
return
}
}
response, err = http.Get(deviceDescriptionLocation) response, err = http.Get(deviceDescriptionLocation)
if err != nil { if err != nil {
l.Infoln(err) return IGD{}, err
return
} }
defer response.Body.Close() defer response.Body.Close()
if response.StatusCode >= 400 { if response.StatusCode >= 400 {
l.Infoln(errors.New(response.Status)) return IGD{}, errors.New("bad status code:" + response.Status)
return
} }
var upnpRoot upnpRoot var upnpRoot upnpRoot
err = xml.NewDecoder(response.Body).Decode(&upnpRoot) err = xml.NewDecoder(response.Body).Decode(&upnpRoot)
if err != nil { if err != nil {
l.Infoln(err) return IGD{}, err
return
} }
services, err := getServiceDescriptions(deviceDescriptionLocation, upnpRoot.Device) services, err := getServiceDescriptions(deviceDescriptionLocation, upnpRoot.Device)
if err != nil { if err != nil {
l.Infoln(err) return IGD{}, err
return
} }
// Figure out our IP number, on the network used to reach the IGD. // Figure out our IP number, on the network used to reach the IGD.
@ -306,23 +288,16 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
// suggestions on a better way to do this... // suggestions on a better way to do this...
localIPAddress, err := localIP(deviceDescriptionURL) localIPAddress, err := localIP(deviceDescriptionURL)
if err != nil { if err != nil {
l.Infoln(err) return IGD{}, err
return
} }
igd := IGD{ return IGD{
uuid: deviceUUID, uuid: deviceUUID,
friendlyName: upnpRoot.Device.FriendlyName, friendlyName: upnpRoot.Device.FriendlyName,
url: deviceDescriptionURL, url: deviceDescriptionURL,
services: services, services: services,
localIPAddress: localIPAddress, localIPAddress: localIPAddress,
} }, nil
resultChannel <- igd
if debug {
l.Debugln("Finished handling of UPnP response.")
}
} }
func localIP(url *url.URL) (string, error) { func localIP(url *url.URL) (string, error) {