167 lines
3.9 KiB
Go
167 lines
3.9 KiB
Go
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
|
|
}
|