173 lines
4.4 KiB
Go
173 lines
4.4 KiB
Go
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
|
|
}
|