package api import ( "errors" "net" "net/http" "net/url" "os" "strconv" "strings" "time" ) type loginSource struct { started time.Time count int } func configuredOrigin(raw string) (string, error) { if raw == "" { return "", nil } u, err := url.Parse(raw) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" || u.Hostname() == "" || u.User != nil || (u.Path != "" && u.Path != "/") || u.RawQuery != "" || u.ForceQuery || u.Fragment != "" { return "", errors.New("invalid PANEL_PUBLIC_URL") } if strings.Contains(raw, "#") || strings.HasSuffix(u.Host, ":") { return "", errors.New("invalid PANEL_PUBLIC_URL") } port := u.Port() if port != "" { n, err := strconv.Atoi(port) if err != nil || n < 1 || n > 65535 { return "", errors.New("invalid PANEL_PUBLIC_URL") } port = strconv.Itoa(n) } host := strings.ToLower(u.Hostname()) if net.ParseIP(host) == nil { if len(host) > 253 { return "", errors.New("invalid PANEL_PUBLIC_URL") } for _, label := range strings.Split(strings.TrimSuffix(host, "."), ".") { if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { return "", errors.New("invalid PANEL_PUBLIC_URL") } for _, c := range label { if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') { return "", errors.New("invalid PANEL_PUBLIC_URL") } } } } else if strings.Contains(host, ":") { host = "[" + host + "]" } if port != "" && !(u.Scheme == "https" && port == "443" || u.Scheme == "http" && port == "80") { host += ":" + port } return u.Scheme + "://" + host, nil } func configuredProxyCIDRs(raw string, prior error) ([]*net.IPNet, error) { if prior != nil || strings.TrimSpace(raw) == "" { return nil, prior } var out []*net.IPNet for _, value := range strings.Split(raw, ",") { value = strings.TrimSpace(value) if value == "" { return nil, errors.New("invalid TRUSTED_PROXY_CIDRS") } _, network, err := net.ParseCIDR(value) if err != nil { return nil, errors.New("invalid TRUSTED_PROXY_CIDRS") } out = append(out, network) } return out, nil } func ValidateAuthConfig() error { _, err := configuredOrigin(os.Getenv("PANEL_PUBLIC_URL")) _, err = configuredProxyCIDRs(os.Getenv("TRUSTED_PROXY_CIDRS"), err) return err } func (a *API) ConfigError() error { return a.loginConfigErr } func (a *API) acquireLoginSlot() bool { a.loginMu.Lock() defer a.loginMu.Unlock() if a.loginSlots == nil { a.loginSlots = make(chan struct{}, 8) } select { case a.loginSlots <- struct{}{}: return true default: return false } } func (a *API) loginSource(r *http.Request) string { peer, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { peer = r.RemoteAddr } ip := net.ParseIP(peer) if ip == nil { return peer } trusted := false for _, network := range a.trustedProxies { if network.Contains(ip) { trusted = true break } } if trusted { parts := strings.Split(r.Header.Get("X-Forwarded-For"), ",") for i := len(parts) - 1; i >= 0; i-- { candidate := net.ParseIP(strings.TrimSpace(parts[i])) if candidate == nil { return ip.String() } isProxy := false for _, network := range a.trustedProxies { if network.Contains(candidate) { isProxy = true break } } if !isProxy { return candidate.String() } } } return ip.String() } func (a *API) allowLoginSource(source string) bool { now := time.Now() a.loginMu.Lock() defer a.loginMu.Unlock() if a.loginSources == nil { a.loginSources = make(map[string]loginSource) } for key, entry := range a.loginSources { if now.Sub(entry.started) >= time.Minute { delete(a.loginSources, key) } } entry, ok := a.loginSources[source] if !ok { if len(a.loginSources) >= 1024 { return false } entry = loginSource{started: now} } if entry.count >= 30 { return false } entry.count++ a.loginSources[source] = entry return true }