package app import ( "context" "errors" "net" "net/http" "net/url" "strconv" "strings" ) func normalizeAccessURL(raw string) (string, error) { u, err := url.Parse(raw) if err != nil || u.Host == "" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") || (u.Scheme != "http" && u.Scheme != "https") { return "", errors.New("访问地址须为无路径的http/https站点地址") } host := strings.ToLower(u.Hostname()) port := u.Port() if port != "" { n, e := strconv.Atoi(port) if e != nil || n < 1 || n > 65535 { return "", errors.New("访问端口须为1至65535") } } if port == "80" && u.Scheme == "http" || port == "443" && u.Scheme == "https" { port = "" } if port != "" { host = net.JoinHostPort(host, port) } else if strings.Contains(host, ":") { host = "[" + host + "]" } return u.Scheme + "://" + host, nil } func (c Config) accessOrigins() []string { out := []string{} for _, raw := range append([]string{c.PublicURL}, c.AccessURLs...) { base, err := normalizeAccessURL(raw) if err != nil { continue } found := false for _, v := range out { if v == base { found = true break } } if !found { out = append(out, base) } } return out } type requestAccessKey struct{} type requestAccess struct { base string httpCookie bool } func (a *App) accessFor(r *http.Request) requestAccess { if state, ok := r.Context().Value(requestAccessKey{}).(requestAccess); ok { return state } return requestAccess{base: strings.TrimRight(a.cfg.PublicURL, "/")} } func (a *App) selectAccess(r *http.Request) (requestAccess, error) { origins := a.cfg.accessOrigins() allowed := func(raw string) string { normalized, err := normalizeAccessURL(raw) if err != nil { return "" } for _, v := range origins { if v == normalized { return v } } return "" } origin, hint := r.Header.Get("Origin"), r.Header.Get("X-Bluebird-Origin") selected := "" if origin != "" { selected = allowed(origin) if selected == "" { return requestAccess{}, errors.New("请求来源不允许") } } if hint != "" { base := allowed(hint) if base == "" || selected != "" && base != selected { return requestAccess{}, errors.New("访问入口不允许") } selected = base } if selected == "" { host, scheme := r.Host, "" if r.TLS != nil { scheme = "https" } peer, _, err := net.SplitHostPort(r.RemoteAddr) ip := net.ParseIP(peer) trusted := false if err == nil { for _, network := range a.proxies { if network.Contains(ip) { trusted = true break } } } if trusted { if forwarded := r.Header.Get("X-Forwarded-Host"); forwarded != "" && !strings.Contains(forwarded, ",") { host = forwarded } if proto := r.Header.Get("X-Forwarded-Proto"); proto == "http" || proto == "https" { scheme = proto } } for _, base := range origins { u, _ := url.Parse(base) if strings.EqualFold(u.Host, host) && (scheme == "" || scheme == u.Scheme) { selected = base break } } } if selected == "" { selected = origins[0] } if r.TLS != nil && strings.HasPrefix(selected, "http://") { return requestAccess{}, errors.New("访问入口协议与连接不一致") } hasHTTPS := false for _, base := range origins { if strings.HasPrefix(base, "https://") { hasHTTPS = true break } } return requestAccess{base: selected, httpCookie: hasHTTPS && strings.HasPrefix(selected, "http://")}, nil } func (a *App) withAccess(r *http.Request) (*http.Request, error) { state, err := a.selectAccess(r) if err != nil { return nil, err } return r.WithContext(context.WithValue(r.Context(), requestAccessKey{}, state)), nil } func cookieNames(r *http.Request, name string) []string { state, _ := r.Context().Value(requestAccessKey{}).(requestAccess) if state.httpCookie && (r.Header.Get("Origin") != "" || r.Header.Get("X-Bluebird-Origin") != "") { return []string{name + "_http", name} } return []string{name, name + "_http"} } // LANListen accepts only a concrete private or loopback HTTP address. func LANListen(raw string) (string, string, error) { base, err := normalizeAccessURL(raw) if err != nil { return "", "", err } u, _ := url.Parse(base) ip := net.ParseIP(u.Hostname()) if u.Scheme != "http" || ip == nil || !ip.IsPrivate() && !ip.IsLoopback() || u.Port() == "" { return "", "", errors.New("内网入口须为http://具体私有IP:端口") } return u.Host, base, nil }