Skip to content

Commit 18b72a7

Browse files
authored
Poll for address of Supervisor default interface for mDNS URL (#165)
* Poll for address of Supervisor default interface for mDNS URL Outstanding comment mentions the hacky way for obtaining the outbound IP address should be replaced by API from home-assistant/supervisor#2115. Poll this API periodically with 5s retry interval in case the IP can't be determined or parsed from the response. This still ignores IPv6-only deployment of HA, however, in that case the landing page should still be reachable, just not discoverable through mDNS (combined with #164). * Implement setSupervisorAuthHeader for setting bearer token
1 parent d09fe59 commit 18b72a7

File tree

3 files changed

+111
-19
lines changed

3 files changed

+111
-19
lines changed

http.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ func httpSupervisorProxy(w http.ResponseWriter, r *http.Request) {
7070
log.Printf("Proxy request: %s", r.URL.Path)
7171

7272
// Base Supervisor URL
73-
supervisorHost := "supervisor"
74-
75-
if development && os.Getenv("SUPERVISOR_HOST") != "" {
76-
supervisorHost = os.Getenv("SUPERVISOR_HOST")
77-
}
73+
supervisorHost := getSupervisorHost()
7874

7975
u, err := url.Parse("http://" + supervisorHost + "/")
8076
if err != nil {
@@ -116,7 +112,7 @@ func httpSupervisorProxy(w http.ResponseWriter, r *http.Request) {
116112
proxy := httputil.NewSingleHostReverseProxy(u)
117113

118114
// Add authorization header
119-
r.Header.Add("Authorization", "Bearer "+os.Getenv("SUPERVISOR_TOKEN"))
115+
setSupervisorAuthHeader(r)
120116

121117
switch cleanPath {
122118
case "/logs":

main.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@ var mdns *zeroconf.Server
1414
var wwwRoot string
1515
var development bool
1616

17+
func getSupervisorHost() string {
18+
supervisorHost := "supervisor"
19+
20+
if development && os.Getenv("SUPERVISOR_HOST") != "" {
21+
supervisorHost = os.Getenv("SUPERVISOR_HOST")
22+
}
23+
24+
return supervisorHost
25+
}
26+
27+
func setSupervisorAuthHeader(r *http.Request) {
28+
token := os.Getenv("SUPERVISOR_TOKEN")
29+
if token != "" {
30+
r.Header.Add("Authorization", "Bearer "+token)
31+
} else {
32+
log.Println("No SUPERVISOR_TOKEN set, request will be unauthenticated")
33+
}
34+
}
35+
1736
func main() {
1837
development = (os.Getenv("DEVELOPMENT") == "True")
1938

mdns.go

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
package main
22

33
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
47
"log"
58
"net"
9+
"net/http"
10+
"time"
611

712
"github.com/grandcat/zeroconf"
813
"github.com/rs/xid"
914
)
1015

16+
type Response struct {
17+
Result string `json:"result"`
18+
Message string `json:"message,omitempty"`
19+
Data map[string]any `json:"data,omitempty"`
20+
}
21+
1122
func publishHomeAssistant() {
1223
var err error
1324

25+
var outboundIP net.IP
26+
27+
for {
28+
outboundIP, err = getOutboundIP()
29+
if outboundIP != nil {
30+
break
31+
}
32+
log.Printf("Failed to get outbound IP, retrying in 5s: %s", err)
33+
time.Sleep(5 * time.Second)
34+
}
35+
1436
unique := xid.New()
15-
hostURL := "http://" + getOutboundIP().String() + ":8123"
37+
hostURL := "http://" + outboundIP.String() + ":8123"
1638
params := []string{
1739
"location_name=Home Assistant",
1840
"uuid=",
@@ -31,21 +53,76 @@ func publishHomeAssistant() {
3153
}
3254
}
3355

34-
// Get preferred outbound ip of this machine
35-
// https://stackoverflow.com/a/37382208
36-
// Can be removed after Supervisor v248 with new lookup
37-
func getOutboundIP() net.IP {
38-
conn, err := net.Dial("udp", "8.8.8.8:80")
56+
// Get the first IPv4 address from the interface information
57+
func getFirstIPv4Address(iface map[string]any) (net.IP, error) {
58+
ipv4Data, ok := iface["ipv4"].(map[string]any)
59+
if !ok {
60+
return nil, fmt.Errorf("no ipv4 addresses found")
61+
}
62+
63+
addresses, ok := ipv4Data["address"].([]any)
64+
if !ok || len(addresses) == 0 {
65+
return nil, fmt.Errorf("empty ipv4 address list")
66+
}
67+
68+
addr, ok := addresses[0].(string)
69+
if !ok {
70+
return nil, fmt.Errorf("can't parse first ipv4 address")
71+
}
72+
73+
ip, _, err := net.ParseCIDR(addr)
74+
3975
if err != nil {
40-
log.Fatal(err)
76+
return nil, fmt.Errorf("invalid IPv4 address: %s", err)
4177
}
42-
defer func() {
43-
if err := conn.Close(); err != nil {
44-
log.Printf("error closing mdns connection: %v", err)
78+
79+
return ip, nil
80+
}
81+
82+
// Get IP address of the default interface from Supervisor
83+
func getOutboundIP() (net.IP, error) {
84+
supervisorHost := getSupervisorHost()
85+
86+
client := &http.Client{}
87+
req, err := http.NewRequest("GET", "http://"+supervisorHost+"/network/interface/default/info", nil)
88+
89+
if err != nil {
90+
return nil, fmt.Errorf("can't create request to Supervisor: %s", err)
91+
}
92+
93+
setSupervisorAuthHeader(req)
94+
95+
response, err := client.Do(req)
96+
if err == nil {
97+
defer response.Body.Close()
98+
}
99+
100+
if err != nil || response.StatusCode >= 300 {
101+
var errorMsg string
102+
103+
if err == nil {
104+
if data, err := io.ReadAll(response.Body); err == nil {
105+
errorMsg = string(data)
106+
} else {
107+
errorMsg = err.Error()
108+
}
109+
} else {
110+
errorMsg = err.Error()
45111
}
46-
}()
47112

48-
localAddr := conn.LocalAddr().(*net.UDPAddr)
113+
return nil, fmt.Errorf("can't get default interface from Supervisor: %s", errorMsg)
114+
}
115+
116+
var responseData Response
117+
118+
if err := json.NewDecoder(response.Body).Decode(&responseData); err != nil {
119+
return nil, fmt.Errorf("can't parse default interface data: %s", err)
120+
}
121+
122+
ipv4Addr, err := getFirstIPv4Address(responseData.Data)
123+
if err != nil {
124+
return nil, err
125+
}
49126

50-
return localAddr.IP
127+
return ipv4Addr, nil
51128
}

0 commit comments

Comments
 (0)