package webui import ( "fmt" "net" "net/url" "strconv" "strings" ) // ParseOrigin reads an origin and returns it in the form a browser writes in an // Origin header: scheme and host only, the host in lower case, a default port // left out. A flag and the header the same page sends then name the same origin // even when one says https://Host:443 and the other https://host. A nil URL is // no origin at all. func ParseOrigin(text string) (*url.URL, error) { trimmed := strings.TrimSpace(text) if trimmed == "" { return nil, fmt.Errorf("no origin given") } parsed, err := url.Parse(trimmed) if err != nil { return nil, fmt.Errorf("origin %q: %w", text, err) } parsed.Scheme = strings.ToLower(parsed.Scheme) if parsed.Scheme != "http" && parsed.Scheme != "https" { return nil, fmt.Errorf("origin %q: scheme must be http or https", text) } if parsed.User != nil { return nil, fmt.Errorf("origin %q: no credentials", text) } if parsed.Hostname() == "" { return nil, fmt.Errorf("origin %q: no host", text) } if parsed.RawQuery != "" || parsed.Fragment != "" || (parsed.Path != "" && parsed.Path != "/") { return nil, fmt.Errorf("origin %q: give only the scheme and host, without a path, query or fragment", text) } parsed.Host = canonicalHost(parsed) parsed.Path, parsed.RawPath = "", "" return parsed, nil } // canonicalHost is u's host as a browser writes an origin: lower case, without // the scheme's default port, and an IPv6 literal in brackets. func canonicalHost(u *url.URL) string { host := strings.ToLower(u.Hostname()) if port := u.Port(); port != "" && port != defaultPort(u.Scheme) { return net.JoinHostPort(host, port) } if strings.Contains(host, ":") { return "[" + host + "]" } return host } // defaultPort is the port a browser leaves out of an origin for scheme. func defaultPort(scheme string) string { if scheme == "http" { return "80" } return "443" } // originPort is the port u is reached on: its own, or the scheme's default. func originPort(u *url.URL) string { if port := u.Port(); port != "" { return port } return defaultPort(u.Scheme) } // SameOrigin reports whether a and b name the same origin; nil is no origin. func SameOrigin(a, b *url.URL) bool { if a == nil || b == nil { return a == nil && b == nil } return a.Scheme == b.Scheme && strings.EqualFold(a.Hostname(), b.Hostname()) && originPort(a) == originPort(b) } // Loopback is the origin of a listener on port at 127.0.0.1. func Loopback(port int) *url.URL { return &url.URL{Scheme: "http", Host: net.JoinHostPort("127.0.0.1", strconv.Itoa(port))} } // LaunchLink is the address that signs a browser in through origin: the key // trades once for the cookie, so the link itself is the secret. func LaunchLink(origin *url.URL, key string) string { link := *origin link.Path = "/" link.RawQuery = url.Values{"key": {key}}.Encode() return link.String() }