package httpsecurity import ( "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "net/http" "net/netip" "strings" "proxy-pool/internal/domain/authorization" ) var ( errCredentialRejected = errors.New("credential rejected") errSourceRejected = errors.New("source rejected") ) type authenticator interface { authenticate(*http.Request, string) (authenticationResult, error) challenges() []string } type authenticationResult struct { Principal string Permissions []string } type noAuthenticator struct{} func (noAuthenticator) authenticate(*http.Request, string) (authenticationResult, error) { return authenticationResult{}, nil } func (noAuthenticator) challenges() []string { return nil } type scopedAuthenticator struct { delegate authenticator permissions []string } func (auth scopedAuthenticator) authenticate(request *http.Request, source string) (authenticationResult, error) { result, err := auth.delegate.authenticate(request, source) if err != nil { return authenticationResult{}, err } result.Permissions = append([]string(nil), auth.permissions...) return result, nil } func (auth scopedAuthenticator) challenges() []string { return auth.delegate.challenges() } type basicAuthenticator struct { header string username string password string } func (auth basicAuthenticator) authenticate(request *http.Request, _ string) (authenticationResult, error) { value, ok := singleHeader(request, auth.header) username, password, parsed := parseBasicCredentials(value) valid := subtle.ConstantTimeSelect(boolInt(ok && parsed), 1, 0) valid &= secureEqual(username, auth.username) valid &= secureEqual(password, auth.password) if valid != 1 { return authenticationResult{}, errCredentialRejected } return authenticationResult{Principal: "basic:" + auth.username}, nil } func (basicAuthenticator) challenges() []string { return []string{`Basic realm="proxy-pool"`} } type tokenAuthenticator struct { mode string header string token string } func (auth tokenAuthenticator) authenticate(request *http.Request, _ string) (authenticationResult, error) { value, ok := singleHeader(request, auth.header) if auth.mode == ModeBearer { value, ok = parseScheme(value, "Bearer", ok) } if !ok || secureEqual(value, auth.token) != 1 { return authenticationResult{}, errCredentialRejected } return authenticationResult{Principal: credentialSubject(auth.mode, auth.token)}, nil } func (auth tokenAuthenticator) challenges() []string { if auth.mode == ModeBearer { return []string{`Bearer realm="proxy-pool"`} } return []string{`ApiKey realm="proxy-pool", header="` + auth.header + `"`} } type ipAuthenticator struct{ allowed cidrMatcher } func (auth ipAuthenticator) authenticate(_ *http.Request, source string) (authenticationResult, error) { address, err := netip.ParseAddr(source) if err != nil || !auth.allowed.match(address) { return authenticationResult{}, errSourceRejected } return authenticationResult{Principal: "source:" + source}, nil } func (ipAuthenticator) challenges() []string { return nil } type anyAuthenticator struct{ methods []authenticator } func (auth anyAuthenticator) authenticate(request *http.Request, source string) (authenticationResult, error) { sourceRejected := false for _, method := range auth.methods { result, err := method.authenticate(request, source) if err == nil { return result, nil } if errors.Is(err, errSourceRejected) { sourceRejected = true } } if sourceRejected { return authenticationResult{}, errSourceRejected } return authenticationResult{}, errCredentialRejected } func (auth anyAuthenticator) challenges() []string { var result []string for _, method := range auth.methods { result = append(result, method.challenges()...) } return result } func buildAuthenticator(authentication Authentication, semantics Semantics) (authenticator, error) { if err := authorization.Validate(authentication.Permissions); err != nil { return nil, ErrInvalidConfig } header := "Authorization" if semantics == ProxySemantics { header = "Proxy-Authorization" } switch authentication.Mode { case "", ModeNone: if len(authentication.Permissions) != 0 { return nil, ErrInvalidConfig } return noAuthenticator{}, nil case ModeUsernamePassword: if authentication.Username == "" || authentication.Password == "" { return nil, ErrInvalidConfig } return scopedAuthenticator{delegate: basicAuthenticator{header: header, username: authentication.Username, password: authentication.Password}, permissions: authentication.Permissions}, nil case ModeAPIKey: if !validHeaderName(authentication.Header) || authentication.Token == "" { return nil, ErrInvalidConfig } return scopedAuthenticator{delegate: tokenAuthenticator{mode: ModeAPIKey, header: authentication.Header, token: authentication.Token}, permissions: authentication.Permissions}, nil case ModeBearer: if authentication.Token == "" { return nil, ErrInvalidConfig } return scopedAuthenticator{delegate: tokenAuthenticator{mode: ModeBearer, header: header, token: authentication.Token}, permissions: authentication.Permissions}, nil case ModeIPWhitelist: allowed, err := newCIDRMatcher(authentication.CIDRs) if err != nil || len(authentication.CIDRs) == 0 { return nil, ErrInvalidConfig } return scopedAuthenticator{delegate: ipAuthenticator{allowed: allowed}, permissions: authentication.Permissions}, nil case ModeAny: if len(authentication.Permissions) != 0 { return nil, ErrInvalidConfig } if len(authentication.Methods) == 0 { return nil, ErrInvalidConfig } methods := make([]authenticator, 0, len(authentication.Methods)) for _, method := range authentication.Methods { candidate, err := buildMethod(method, semantics) if err != nil { return nil, err } methods = append(methods, candidate) } return anyAuthenticator{methods: methods}, nil default: return nil, ErrInvalidConfig } } func buildMethod(method Method, semantics Semantics) (authenticator, error) { if err := authorization.Validate(method.Permissions); err != nil { return nil, ErrInvalidConfig } authentication := Authentication{ Mode: method.Mode, Permissions: method.Permissions, Username: method.Username, Password: method.Password, Header: method.Header, Token: method.Value, CIDRs: method.CIDRs, } if method.Mode == ModeAny || method.Mode == ModeNone || method.Mode == "" { return nil, ErrInvalidConfig } return buildAuthenticator(authentication, semantics) } func parseBasicCredentials(value string) (string, string, bool) { encoded, ok := parseScheme(value, "Basic", true) if !ok { return "", "", false } decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", "", false } username, password, ok := strings.Cut(string(decoded), ":") return username, password, ok } func parseScheme(value, expected string, present bool) (string, bool) { if !present { return "", false } fields := strings.Fields(value) if len(fields) != 2 || !strings.EqualFold(fields[0], expected) { return "", false } return fields[1], true } func singleHeader(request *http.Request, name string) (string, bool) { if request == nil { return "", false } values := request.Header.Values(name) return first(values), len(values) == 1 && values[0] != "" } func first(values []string) string { if len(values) == 0 { return "" } return values[0] } func secureEqual(actual, expected string) int { actualHash := sha256.Sum256([]byte(actual)) expectedHash := sha256.Sum256([]byte(expected)) return subtle.ConstantTimeCompare(actualHash[:], expectedHash[:]) } func credentialSubject(kind, credential string) string { digest := sha256.Sum256([]byte(credential)) return kind + ":" + hex.EncodeToString(digest[:16]) } func boolInt(value bool) int { if value { return 1 } return 0 } func validHeaderName(value string) bool { if value == "" { return false } for _, character := range []byte(value) { if !isTokenCharacter(character) { return false } } return true } func isTokenCharacter(character byte) bool { return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9' || strings.ContainsRune("!#$%&'*+-.^_`|~", rune(character)) }