proxy-pool/internal/platform/httpsecurity/protection.go

123 lines
4.1 KiB
Go

package httpsecurity
import (
"context"
"errors"
"net/http"
)
type Protection struct {
resolver *ClientIPResolver
allow cidrMatcher
allowAll bool
authentication authenticator
clientMode string
semantics Semantics
admitter Admitter
}
func New(config Config, admitter Admitter) (*Protection, error) {
if config.Semantics != APIAuthSemantics && config.Semantics != ProxySemantics {
return nil, ErrInvalidConfig
}
resolver, err := NewClientIPResolver(config.TrustedProxies)
if err != nil {
return nil, ErrInvalidConfig
}
allow, err := newCIDRMatcher(config.AllowCIDRs)
if err != nil {
return nil, ErrInvalidConfig
}
authentication, err := buildAuthenticator(config.Authentication, config.Semantics)
if err != nil {
return nil, ErrInvalidConfig
}
clientMode := config.ClientIdentification
if clientMode == "" {
clientMode = ClientSourceIP
}
if clientMode != ClientSourceIP && clientMode != ClientAuthenticated && clientMode != ClientAuthenticatedOrSourceIP {
return nil, ErrInvalidConfig
}
if clientMode == ClientAuthenticated && (config.Authentication.Mode == "" || config.Authentication.Mode == ModeNone) {
return nil, ErrInvalidConfig
}
return &Protection{
resolver: resolver, allow: allow, allowAll: len(config.AllowCIDRs) == 0,
authentication: authentication, clientMode: clientMode, semantics: config.Semantics,
admitter: admitter,
}, nil
}
func (protection *Protection) Resolve(request *http.Request) (Identity, error) {
if request == nil {
return Identity{}, newHTTPError(http.StatusBadRequest, "INVALID_SOURCE", nil, errors.New("request is required"))
}
return protection.evaluate(request.Context(), request)
}
func (protection *Protection) Check(ctx context.Context, request *http.Request) error {
_, err := protection.evaluate(ctx, request)
return err
}
func (protection *Protection) evaluate(ctx context.Context, request *http.Request) (Identity, error) {
if protection == nil || protection.resolver == nil || protection.authentication == nil || request == nil {
return Identity{}, newHTTPError(http.StatusInternalServerError, "SECURITY_NOT_CONFIGURED", nil, ErrInvalidConfig)
}
address, err := protection.resolver.Resolve(request)
if err != nil {
return Identity{}, newHTTPError(http.StatusBadRequest, "INVALID_SOURCE", nil, err)
}
if !protection.allowAll && !protection.allow.match(address) {
return Identity{}, newHTTPError(http.StatusForbidden, "FORBIDDEN", nil, errSourceRejected)
}
source := address.String()
authentication, err := protection.authentication.authenticate(request, source)
if err != nil {
if errors.Is(err, errSourceRejected) {
return Identity{}, newHTTPError(http.StatusForbidden, "FORBIDDEN", nil, err)
}
return Identity{}, protection.unauthorized(err)
}
identity := Identity{SourceIP: source, Permissions: append([]string(nil), authentication.Permissions...)}
switch protection.clientMode {
case ClientSourceIP:
identity.ClientID = "source:" + source
case ClientAuthenticated:
if authentication.Principal == "" {
return Identity{}, protection.unauthorized(errCredentialRejected)
}
identity.ClientID = authentication.Principal
case ClientAuthenticatedOrSourceIP:
identity.ClientID = authentication.Principal
if identity.ClientID == "" {
identity.ClientID = "source:" + source
}
}
if protection.admitter != nil {
if err := protection.admitter.Admit(ctx, identity.ClientID); err != nil {
return Identity{}, newHTTPError(http.StatusTooManyRequests, "RATE_LIMITED", nil, err)
}
}
return identity, nil
}
func (protection *Protection) unauthorized(cause error) *HTTPError {
status := http.StatusUnauthorized
headerName := "WWW-Authenticate"
if protection.semantics == ProxySemantics {
status = http.StatusProxyAuthRequired
headerName = "Proxy-Authenticate"
}
header := make(http.Header)
for _, challenge := range protection.authentication.challenges() {
header.Add(headerName, challenge)
}
return newHTTPError(status, "UNAUTHORIZED", header, cause)
}
func newHTTPError(status int, code string, header http.Header, cause error) *HTTPError {
return &HTTPError{StatusCode: status, Code: code, Header: header, Cause: cause}
}