Files

293 lines
7.7 KiB
Go
Raw Permalink Normal View History

2026-09-23 09:40:59 +08:00
package proxy
import (
"encoding/json"
"fmt"
"strings"
"github.com/fonu/fonu/internal/validate"
)
type BasicAuthConfig struct {
Enabled bool `json:"enabled,omitempty"`
Username string `json:"username,omitempty"`
PasswordHash string `json:"password_hash,omitempty"` // bcrypt htpasswd line body
HasPassword bool `json:"has_password,omitempty"` // API response only
}
type RateLimitConfig struct {
Enabled bool `json:"enabled,omitempty"`
Rate int `json:"rate,omitempty"` // requests per second
Burst int `json:"burst,omitempty"` // burst count
}
type ConnLimitConfig struct {
Enabled bool `json:"enabled,omitempty"`
Max int `json:"max,omitempty"` // max concurrent connections per IP
}
type SecurityConfig struct {
IPBlacklist []string `json:"ip_blacklist,omitempty"`
IPWhitelist []string `json:"ip_whitelist,omitempty"`
IPWhitelistMode bool `json:"ip_whitelist_mode,omitempty"`
ChinaOnly bool `json:"china_only,omitempty"`
BasicAuth *BasicAuthConfig `json:"basic_auth,omitempty"`
RateLimit *RateLimitConfig `json:"rate_limit,omitempty"`
ConnLimit *ConnLimitConfig `json:"conn_limit,omitempty"`
ProxySSLVerifyOff bool `json:"proxy_ssl_verify_off,omitempty"`
ProxyHostUpstream bool `json:"proxy_host_upstream,omitempty"` // use $proxy_host instead of $host
TLSMin13Only bool `json:"tls_min_13_only,omitempty"`
SecurityHeaders bool `json:"security_headers,omitempty"`
}
type SecurityInput struct {
IPBlacklist []string
IPWhitelist []string
IPWhitelistMode *bool
ChinaOnly *bool
BasicAuth *BasicAuthInput
RateLimit *RateLimitConfig
ConnLimit *ConnLimitConfig
ProxySSLVerifyOff *bool
ProxyHostUpstream *bool
TLSMin13Only *bool
SecurityHeaders *bool
}
type BasicAuthInput struct {
Enabled *bool
Username *string
Password *string // plaintext on write only
}
// DefaultPrivateCIDRs are always exempt from china_only restrictions.
var DefaultPrivateCIDRs = []string{
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"::1/128",
"fc00::/7",
}
func ChinaBypassCIDRs(extra []string) []string {
seen := make(map[string]bool, len(DefaultPrivateCIDRs)+len(extra))
out := make([]string, 0, len(DefaultPrivateCIDRs)+len(extra))
for _, cidr := range DefaultPrivateCIDRs {
if !seen[cidr] {
seen[cidr] = true
out = append(out, cidr)
}
}
for _, cidr := range extra {
cidr = strings.TrimSpace(cidr)
if cidr != "" && !seen[cidr] {
seen[cidr] = true
out = append(out, cidr)
}
}
return out
}
func DefaultSecurityConfig() SecurityConfig {
return SecurityConfig{}
}
func ParseSecurityJSON(raw string) (SecurityConfig, error) {
raw = strings.TrimSpace(raw)
if raw == "" || raw == "{}" {
return DefaultSecurityConfig(), nil
}
var cfg SecurityConfig
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
return SecurityConfig{}, fmt.Errorf("安全配置格式无效")
}
return cfg.Normalize(), nil
}
func (c SecurityConfig) Normalize() SecurityConfig {
if c.RateLimit != nil && c.RateLimit.Enabled {
if c.RateLimit.Rate <= 0 {
c.RateLimit.Rate = 10
}
if c.RateLimit.Burst <= 0 {
c.RateLimit.Burst = 20
}
}
if c.ConnLimit != nil && c.ConnLimit.Enabled {
if c.ConnLimit.Max <= 0 {
c.ConnLimit.Max = 20
}
}
return c
}
func RuleForAPI(rule Rule) Rule {
rule.Security = rule.Security.ForAPI()
return rule
}
func RulesForAPI(rules []Rule) []Rule {
out := make([]Rule, len(rules))
for i, rule := range rules {
out[i] = RuleForAPI(rule)
}
return out
}
func (c SecurityConfig) ForAPI() SecurityConfig {
out := c.Normalize()
if out.BasicAuth != nil {
hasPassword := out.BasicAuth.PasswordHash != ""
out.BasicAuth = &BasicAuthConfig{
Enabled: out.BasicAuth.Enabled,
Username: out.BasicAuth.Username,
HasPassword: hasPassword,
}
}
return out
}
func (c SecurityConfig) MarshalJSON() ([]byte, error) {
type alias SecurityConfig
return json.Marshal(alias(c.Normalize()))
}
func (c SecurityConfig) ToJSON() (string, error) {
b, err := c.MarshalJSON()
if err != nil {
return "", err
}
return string(b), nil
}
func (c SecurityConfig) BasicAuthEnabled() bool {
return c.BasicAuth != nil && c.BasicAuth.Enabled && c.BasicAuth.Username != "" && c.BasicAuth.PasswordHash != ""
}
func (c SecurityConfig) RateLimitEnabled() bool {
return c.RateLimit != nil && c.RateLimit.Enabled
}
func (c SecurityConfig) ConnLimitEnabled() bool {
return c.ConnLimit != nil && c.ConnLimit.Enabled
}
func (c SecurityConfig) WhitelistEnabled() bool {
return c.IPWhitelistMode && len(c.IPWhitelist) > 0
}
func MergeSecurity(current SecurityConfig, in SecurityInput) (SecurityConfig, error) {
out := current.Normalize()
if in.IPBlacklist != nil {
out.IPBlacklist = in.IPBlacklist
}
if in.IPWhitelist != nil {
out.IPWhitelist = in.IPWhitelist
}
if in.IPWhitelistMode != nil {
out.IPWhitelistMode = *in.IPWhitelistMode
}
if in.ChinaOnly != nil {
out.ChinaOnly = *in.ChinaOnly
}
if in.RateLimit != nil {
out.RateLimit = in.RateLimit
}
if in.ConnLimit != nil {
out.ConnLimit = in.ConnLimit
}
if in.ProxySSLVerifyOff != nil {
out.ProxySSLVerifyOff = *in.ProxySSLVerifyOff
}
if in.ProxyHostUpstream != nil {
out.ProxyHostUpstream = *in.ProxyHostUpstream
}
if in.TLSMin13Only != nil {
out.TLSMin13Only = *in.TLSMin13Only
}
if in.SecurityHeaders != nil {
out.SecurityHeaders = *in.SecurityHeaders
}
if in.BasicAuth != nil {
if in.BasicAuth.Enabled != nil {
if out.BasicAuth == nil {
out.BasicAuth = &BasicAuthConfig{}
}
out.BasicAuth.Enabled = *in.BasicAuth.Enabled
}
if in.BasicAuth.Username != nil {
if out.BasicAuth == nil {
out.BasicAuth = &BasicAuthConfig{}
}
out.BasicAuth.Username = strings.TrimSpace(*in.BasicAuth.Username)
}
if in.BasicAuth.Password != nil && strings.TrimSpace(*in.BasicAuth.Password) != "" {
if out.BasicAuth == nil {
out.BasicAuth = &BasicAuthConfig{}
}
hash, err := validate.BasicAuthPasswordHash(out.BasicAuth.Username, *in.BasicAuth.Password)
if err != nil {
return SecurityConfig{}, err
}
out.BasicAuth.PasswordHash = hash
}
}
return out, ValidateSecurity(out)
}
func ValidateSecurity(cfg SecurityConfig) error {
cfg = cfg.Normalize()
if err := validate.IPOrCIDRList(cfg.IPBlacklist); err != nil {
return fmt.Errorf("IP 黑名单:%w", err)
}
if err := validate.IPOrCIDRList(cfg.IPWhitelist); err != nil {
return fmt.Errorf("IP 白名单:%w", err)
}
if cfg.IPWhitelistMode && len(cfg.IPWhitelist) == 0 {
return fmt.Errorf("启用白名单模式时至少添加一条 IP 或 CIDR")
}
if cfg.BasicAuth != nil && cfg.BasicAuth.Enabled {
if err := validate.Username(cfg.BasicAuth.Username); err != nil {
return fmt.Errorf("Basic Auth:%w", err)
}
if cfg.BasicAuth.PasswordHash == "" {
return fmt.Errorf("Basic Auth 需要设置密码")
}
}
if cfg.RateLimit != nil && cfg.RateLimit.Enabled {
if cfg.RateLimit.Rate < 1 || cfg.RateLimit.Rate > 10000 {
return fmt.Errorf("限流速率无效(1-10000 次/秒)")
}
if cfg.RateLimit.Burst < 1 || cfg.RateLimit.Burst > 100000 {
return fmt.Errorf("限流突发值无效(1-100000)")
}
}
if cfg.ConnLimit != nil && cfg.ConnLimit.Enabled {
if cfg.ConnLimit.Max < 1 || cfg.ConnLimit.Max > 10000 {
return fmt.Errorf("连接数限制无效(1-10000)")
}
}
return nil
}
func ParseIPListText(text string) []string {
lines := strings.Split(text, "\n")
out := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
out = append(out, line)
}
return out
}
func FormatIPListText(list []string) string {
return strings.Join(list, "\n")
}