forked from savaki/go.hue
-
Notifications
You must be signed in to change notification settings - Fork 5
/
upnp.go
117 lines (98 loc) · 3.09 KB
/
upnp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package hue
import "time"
import "net"
import "strings"
import "errors"
const upnpTimeout = 3 * time.Second
// SSDP Payload - Make sure to keep linebreaks and indention untouched.
const ssdpPayload = `M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
ST: ssdp:all
MAN: ssdp:discover
MX: 2
`
func upnpDiscover(respondingHosts chan<- string) error {
// Open listening port for incoming responses
socket, err := net.ListenUDP("udp4", &net.UDPAddr{Port: 1900})
if err != nil {
return err
}
socket.SetDeadline(time.Now().Add(upnpTimeout))
defer socket.Close()
// Send out discovery request as broadcast
rawBody := []byte(strings.Replace(ssdpPayload, "\n", "\r\n", -1))
_, err = socket.WriteToUDP(rawBody, &net.UDPAddr{IP: net.IPv4(239, 255, 255, 250), Port: 1900})
if err != nil {
return err
}
// Loop over responses until timeout hits
var origins []string // keep track of response origins (return each origin only once)
loop:
for {
// Read response
buf := make([]byte, 8192)
_, addr, err := socket.ReadFromUDP(buf)
if err != nil {
if e, ok := err.(net.Error); !ok || !e.Timeout() {
return err //legitimate error, not a timeout.
}
return nil // timeout
}
// Parse and validate response
body := string(buf)
valid, err := ssdpResponseValid(body, addr.IP)
if err != nil || !valid {
continue // Ignore response
}
// Filter responses from duplicate origins
for _, origin := range origins {
if origin == addr.IP.String() {
continue loop // duplicate
}
}
// Response seems valid and unique -> send to channel
origins = append(origins, addr.IP.String())
respondingHosts <- addr.IP.String()
}
}
func ssdpResponseValid(body string, origin net.IP) (valid bool, err error) {
/*
Response example:
HTTP/1.1 200 OK
HOST: 239.255.255.250:1900
EXT:
CACHE-CONTROL: max-age=100
LOCATION: http://192.168.178.241:80/description.xml
SERVER: FreeRTOS/7.4.2 UPnP/1.0 IpBridge/1.10.0
hue-bridgeid: 001788FFFE09A206
ST: upnp:rootdevice
USN: uuid:2f402f80-da50-11e1-9b23-00178809a206::upnp:rootdevice
FROM: https://developers.meethue.com/documentation/changes-bridge-discovery
*/
// Validate header
if !strings.Contains(body, "HTTP/1.1 200 OK") {
return false, errors.New("Invalid SSDP response header")
}
lower := strings.ToLower(body)
// Validate MUST fields (from UPnP Device Architecture 1.1)
if !strings.Contains(lower, "usn") || !strings.Contains(lower, "st") {
return false, errors.New("Invalid SSDP response")
}
// Hue bridges send string "IpBridge" in SERVER field
// (see https://developers.meethue.com/documentation/hue-bridge-discovery)
if !strings.Contains(lower, "ipbridge") {
return false, errors.New("Origin is no hue bridge")
}
// Validate IP in LOCATION field
if !strings.Contains(lower, "location") {
return false, errors.New("Invalid hue bridge response")
}
s := strings.SplitAfter(lower, "location: ")
location := strings.Split(s[1], "\n")[0]
s = strings.SplitAfter(location, "http://")
ip := strings.Split(s[1], ":")[0]
if ip != origin.String() {
return false, errors.New("Response and sender mismatch")
}
return true, nil
}