301 lines
8.2 KiB
Go
301 lines
8.2 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/proxy-pool/proxy-pool/internal/gateway/policy"
|
|
)
|
|
|
|
type HTTPError struct {
|
|
StatusCode int
|
|
Header http.Header
|
|
Cause error
|
|
}
|
|
|
|
func (err *HTTPError) Error() string {
|
|
if err.Cause != nil {
|
|
return err.Cause.Error()
|
|
}
|
|
return http.StatusText(err.StatusCode)
|
|
}
|
|
|
|
func (err *HTTPError) Unwrap() error { return err.Cause }
|
|
|
|
type BasicAuthGuard struct {
|
|
username string
|
|
password string
|
|
}
|
|
|
|
func NewBasicAuthGuard(username, password string) *BasicAuthGuard {
|
|
return &BasicAuthGuard{username: username, password: password}
|
|
}
|
|
|
|
func (guard *BasicAuthGuard) Check(_ context.Context, request *http.Request) error {
|
|
username, password, ok := parseBasicCredentials(request.Header.Get("Proxy-Authorization"))
|
|
if ok && constantTimeEqual(username, guard.username) && constantTimeEqual(password, guard.password) {
|
|
return nil
|
|
}
|
|
return &HTTPError{
|
|
StatusCode: http.StatusProxyAuthRequired,
|
|
Header: http.Header{"Proxy-Authenticate": []string{`Basic realm="proxy"`}},
|
|
Cause: errors.New("proxy authentication failed"),
|
|
}
|
|
}
|
|
|
|
type APIKeyGuard struct {
|
|
header string
|
|
value string
|
|
}
|
|
|
|
func NewAPIKeyGuard(header, value string) *APIKeyGuard {
|
|
if strings.TrimSpace(header) == "" {
|
|
header = "X-API-Key"
|
|
}
|
|
return &APIKeyGuard{header: header, value: value}
|
|
}
|
|
|
|
func (guard *APIKeyGuard) Check(_ context.Context, request *http.Request) error {
|
|
if constantTimeEqual(request.Header.Get(guard.header), guard.value) {
|
|
return nil
|
|
}
|
|
return &HTTPError{StatusCode: http.StatusProxyAuthRequired, Cause: errors.New("proxy API key authentication failed")}
|
|
}
|
|
|
|
type AnyGuard struct {
|
|
guards []Guard
|
|
}
|
|
|
|
func NewAnyGuard(guards ...Guard) *AnyGuard {
|
|
return &AnyGuard{guards: append([]Guard(nil), guards...)}
|
|
}
|
|
|
|
func (guard *AnyGuard) Check(ctx context.Context, request *http.Request) error {
|
|
var lastErr error
|
|
for _, candidate := range guard.guards {
|
|
if candidate == nil {
|
|
continue
|
|
}
|
|
if err := candidate.Check(ctx, request); err == nil {
|
|
return nil
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
}
|
|
if lastErr == nil {
|
|
lastErr = errors.New("no authentication method is configured")
|
|
}
|
|
return lastErr
|
|
}
|
|
|
|
type ClientIPResolver struct {
|
|
trusted policy.CIDRMatcher
|
|
}
|
|
|
|
func NewClientIPResolver(trustedCIDRs []string) (*ClientIPResolver, error) {
|
|
trusted, err := policy.NewCIDRMatcher(trustedCIDRs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create client IP resolver: %w", err)
|
|
}
|
|
return &ClientIPResolver{trusted: trusted}, nil
|
|
}
|
|
|
|
func (resolver *ClientIPResolver) Resolve(request *http.Request) (netip.Addr, error) {
|
|
if resolver == nil || request == nil {
|
|
return netip.Addr{}, errors.New("resolve client IP: resolver and request are required")
|
|
}
|
|
peer, err := parseRemoteAddress(request.RemoteAddr)
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
if !resolver.trusted.Match(peer) {
|
|
return peer, nil
|
|
}
|
|
|
|
chain, present, err := parseForwardedChain(request.Header.Values("Forwarded"))
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
if !present {
|
|
chain, err = parseXForwardedFor(request.Header.Values("X-Forwarded-For"))
|
|
if err != nil {
|
|
return netip.Addr{}, err
|
|
}
|
|
}
|
|
if len(chain) == 0 {
|
|
return peer, nil
|
|
}
|
|
for index := len(chain) - 1; index >= 0; index-- {
|
|
if !resolver.trusted.Match(chain[index]) {
|
|
return chain[index], nil
|
|
}
|
|
}
|
|
if len(chain) > 0 {
|
|
return chain[0], nil
|
|
}
|
|
return peer, nil
|
|
}
|
|
|
|
func parseXForwardedFor(fields []string) ([]netip.Addr, error) {
|
|
chain := make([]netip.Addr, 0, len(fields)+1)
|
|
for _, field := range fields {
|
|
for value := range strings.SplitSeq(field, ",") {
|
|
address, err := netip.ParseAddr(strings.TrimSpace(value))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve client IP: invalid X-Forwarded-For address %q", value)
|
|
}
|
|
chain = append(chain, address.Unmap())
|
|
}
|
|
}
|
|
return chain, nil
|
|
}
|
|
|
|
func parseForwardedChain(fields []string) ([]netip.Addr, bool, error) {
|
|
if len(fields) == 0 {
|
|
return nil, false, nil
|
|
}
|
|
chain := make([]netip.Addr, 0, len(fields)+1)
|
|
for _, field := range fields {
|
|
for element := range strings.SplitSeq(field, ",") {
|
|
found := false
|
|
for parameter := range strings.SplitSeq(element, ";") {
|
|
name, value, ok := strings.Cut(strings.TrimSpace(parameter), "=")
|
|
if !ok || !strings.EqualFold(name, "for") {
|
|
continue
|
|
}
|
|
address, err := parseForwardedIdentifier(value)
|
|
if err != nil {
|
|
return nil, true, err
|
|
}
|
|
chain = append(chain, address)
|
|
found = true
|
|
break
|
|
}
|
|
if !found {
|
|
return nil, true, errors.New("resolve client IP: Forwarded element is missing for parameter")
|
|
}
|
|
}
|
|
}
|
|
return chain, true, nil
|
|
}
|
|
|
|
func parseForwardedIdentifier(raw string) (netip.Addr, error) {
|
|
value := strings.TrimSpace(raw)
|
|
if strings.HasPrefix(value, `"`) {
|
|
unquoted, err := strconv.Unquote(value)
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("resolve client IP: invalid quoted Forwarded identifier")
|
|
}
|
|
value = unquoted
|
|
}
|
|
if strings.EqualFold(value, "unknown") || strings.HasPrefix(value, "_") {
|
|
return netip.Addr{}, fmt.Errorf("resolve client IP: non-IP Forwarded identifier")
|
|
}
|
|
if strings.HasPrefix(value, "[") {
|
|
closing := strings.IndexByte(value, ']')
|
|
if closing < 0 {
|
|
return netip.Addr{}, fmt.Errorf("resolve client IP: invalid Forwarded IPv6 identifier")
|
|
}
|
|
value = value[1:closing]
|
|
} else if host, _, err := net.SplitHostPort(value); err == nil {
|
|
value = host
|
|
}
|
|
address, err := netip.ParseAddr(value)
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("resolve client IP: invalid Forwarded address")
|
|
}
|
|
return address.Unmap(), nil
|
|
}
|
|
|
|
type AccessGuard struct {
|
|
resolver *ClientIPResolver
|
|
allow policy.CIDRMatcher
|
|
unrestricted bool
|
|
}
|
|
|
|
func NewAccessGuard(resolver *ClientIPResolver, allowCIDRs []string) (*AccessGuard, error) {
|
|
if resolver == nil {
|
|
return nil, errors.New("create access guard: client IP resolver is required")
|
|
}
|
|
allow, err := policy.NewCIDRMatcher(allowCIDRs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create access guard: %w", err)
|
|
}
|
|
return &AccessGuard{resolver: resolver, allow: allow, unrestricted: len(allowCIDRs) == 0}, nil
|
|
}
|
|
|
|
func (guard *AccessGuard) Check(_ context.Context, request *http.Request) error {
|
|
address, err := guard.resolver.Resolve(request)
|
|
if err != nil {
|
|
return &HTTPError{StatusCode: http.StatusBadRequest, Cause: err}
|
|
}
|
|
if guard.unrestricted || guard.allow.Match(address) {
|
|
return nil
|
|
}
|
|
return &HTTPError{StatusCode: http.StatusForbidden, Cause: fmt.Errorf("client address %s is not allowed", address)}
|
|
}
|
|
|
|
type Admitter interface {
|
|
Admit(context.Context, string) error
|
|
}
|
|
|
|
type AdmissionGuard struct {
|
|
resolver *ClientIPResolver
|
|
admitter Admitter
|
|
}
|
|
|
|
func NewAdmissionGuard(resolver *ClientIPResolver, admitter Admitter) *AdmissionGuard {
|
|
return &AdmissionGuard{resolver: resolver, admitter: admitter}
|
|
}
|
|
|
|
func (guard *AdmissionGuard) Check(ctx context.Context, request *http.Request) error {
|
|
if guard == nil || guard.resolver == nil || guard.admitter == nil {
|
|
return &HTTPError{StatusCode: http.StatusInternalServerError, Cause: errors.New("gateway admission is not configured")}
|
|
}
|
|
address, err := guard.resolver.Resolve(request)
|
|
if err != nil {
|
|
return &HTTPError{StatusCode: http.StatusBadRequest, Cause: err}
|
|
}
|
|
if err := guard.admitter.Admit(ctx, address.String()); err != nil {
|
|
return &HTTPError{StatusCode: http.StatusTooManyRequests, Cause: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseBasicCredentials(value string) (string, string, bool) {
|
|
scheme, encoded, ok := strings.Cut(strings.TrimSpace(value), " ")
|
|
if !ok || !strings.EqualFold(scheme, "Basic") {
|
|
return "", "", false
|
|
}
|
|
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(encoded))
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
username, password, ok := strings.Cut(string(decoded), ":")
|
|
return username, password, ok
|
|
}
|
|
|
|
func constantTimeEqual(actual, expected string) bool {
|
|
return subtle.ConstantTimeCompare([]byte(actual), []byte(expected)) == 1
|
|
}
|
|
|
|
func parseRemoteAddress(remote string) (netip.Addr, error) {
|
|
host, _, err := net.SplitHostPort(strings.TrimSpace(remote))
|
|
if err != nil {
|
|
host = strings.TrimSpace(remote)
|
|
}
|
|
address, err := netip.ParseAddr(host)
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("resolve client IP: invalid remote address %q", remote)
|
|
}
|
|
return address.Unmap(), nil
|
|
}
|