package webui import ( "fmt" "net" "net/http" "net/url" "strings" ) // Access restricts listener hosts, browser origins and shared client networks. // Configure it before serving requests. type Access struct { hosts map[string]bool public *url.URL clients []*net.IPNet } func NewAccess() *Access { return &Access{hosts: map[string]bool{"127.0.0.1": true, "localhost": true}} } // Accept reports whether the request's host, network and browser origin are allowed. func (s *Access) Accept(r *http.Request) bool { return s.allowed(r.Host) && s.client(r.RemoteAddr) && (r.Header.Get("Origin") == "" || s.ownOrigin(r.Header.Get("Origin"), r.Host)) } // Share permits clients on these networks and returns matching local addresses. func (s *Access) Share(networks []*net.IPNet, addresses []string) []string { s.clients = append(s.clients, networks...) var shared []string for _, address := range addresses { if ip := net.ParseIP(address); ip != nil && inside(networks, ip) { s.hosts[address] = true shared = append(shared, address) } } return shared } // Answer also accepts these names as the `Host` of a request, for the names // this machine is reached by on a network it shares to (Tailscale's MagicDNS). func (s *Access) Answer(names ...string) { for _, name := range names { s.hosts[strings.ToLower(name)] = true } } // Publish accepts the origin and hostname of a reverse proxy in front. func (s *Access) Publish(origin *url.URL) { s.public = origin s.Answer(origin.Hostname()) } // ownOrigin accepts the request host or the published reverse-proxy origin. func (s *Access) ownOrigin(origin, host string) bool { parsed, err := ParseOrigin(origin) if err != nil { return false } // The Host header is the same origin when it names the same host and port; // a proxy may forward the default port the browser left out of the origin // (nginx's $host:$server_port), and an absent port means the scheme's own. request := &url.URL{Scheme: parsed.Scheme, Host: host} return SameOrigin(parsed, request) || SameOrigin(parsed, s.public) } // allowed refuses other names that resolve here (DNS rebinding). func (s *Access) allowed(hostport string) bool { host, _, err := net.SplitHostPort(hostport) if err != nil { host = hostport } return s.hosts[strings.ToLower(strings.Trim(host, "[]"))] } // client lets this machine in, and devices on the shared networks. Unshared, // the listener is on loopback alone and every client is this machine. func (s *Access) client(remote string) bool { if len(s.clients) == 0 { return true } host, _, err := net.SplitHostPort(remote) if err != nil { host = remote } ip := net.ParseIP(host) return ip == nil || ip.IsLoopback() || inside(s.clients, ip) } func inside(networks []*net.IPNet, ip net.IP) bool { for _, network := range networks { if network.Contains(ip) { return true } } return false } // ParseNetwork reads a network as CIDR, or as an IPv4 address whose trailing // zero octets stand for any: 10.0.0.0 is 10.0.0.0/8, 100.64.0.0 is // 100.64.0.0/16, and an address with no zero octets is that address alone. func ParseNetwork(text string) (*net.IPNet, error) { if strings.Contains(text, "/") { _, network, err := net.ParseCIDR(text) return network, err } ip := net.ParseIP(text).To4() if ip == nil { return nil, fmt.Errorf("%q is not an IPv4 address or a CIDR network", text) } bits := 32 for octet := 3; octet >= 0 && ip[octet] == 0; octet-- { bits -= 8 } return &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, 32)}, nil } // NetworkAddresses are this machine's addresses other devices can reach: up // interfaces' IPv4 addresses, without loopback and link-local ones. func NetworkAddresses() ([]string, error) { interfaces, err := net.Interfaces() if err != nil { return nil, err } var found []string for _, iface := range interfaces { if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } addresses, err := iface.Addrs() if err != nil { continue } for _, address := range addresses { network, ok := address.(*net.IPNet) if !ok || network.IP.To4() == nil || network.IP.IsLinkLocalUnicast() { continue } found = append(found, network.IP.String()) } } return found, nil }