proxy-pool/internal/gateway/server/routing.go

112 lines
3.6 KiB
Go

package server
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
"proxy-pool/internal/domain/routing"
"proxy-pool/internal/gateway/dispatch"
"proxy-pool/internal/gateway/snapshot"
"proxy-pool/internal/platform/httpsecurity"
)
var (
ErrRouteNotFound = errors.New("no gateway routing rule matched")
ErrRouteRejected = errors.New("gateway routing rule rejected the request")
ErrRouteForbidden = errors.New("gateway routing is not permitted for the authenticated client")
ErrDirectRouteUnsupported = errors.New("direct gateway routing is not implemented")
)
type RulesRouter struct {
rules *routing.RuleSet
}
func authorizeClientRouting(request *http.Request, route dispatch.Request) error {
identity, authenticated := httpsecurity.IdentityFromRequest(request)
if !authenticated || identity.ClientPolicy.AllowsRouting(route.RoutingName) {
return nil
}
return &HTTPError{StatusCode: http.StatusForbidden, Cause: ErrRouteForbidden}
}
func NewRulesRouter(rules *routing.RuleSet) *RulesRouter {
return &RulesRouter{rules: rules}
}
func (router *RulesRouter) Route(request *http.Request) (dispatch.Request, error) {
if router == nil || router.rules == nil || request == nil {
return dispatch.Request{}, ErrRouteNotFound
}
return routeWithMatcher(request, router.rules.Match)
}
// SnapshotRouter always matches against the rules stored with the current
// proxy snapshot so a request cannot combine routing from one version with
// proxy ownership from another.
type SnapshotRouter struct {
store *snapshot.Store
}
func NewSnapshotRouter(store *snapshot.Store) *SnapshotRouter {
return &SnapshotRouter{store: store}
}
func (router *SnapshotRouter) Route(request *http.Request) (dispatch.Request, error) {
if router == nil || router.store == nil || request == nil {
return dispatch.Request{}, ErrRouteNotFound
}
view := router.store.Current()
if view == nil || (!view.ValidUntil.IsZero() && !view.ValidUntil.After(time.Now().UTC())) {
return dispatch.Request{}, ErrRouteNotFound
}
return routeWithMatcher(request, view.MatchRouting)
}
type routeMatcher func(routing.Request) (routing.Rule, bool)
func routeWithMatcher(request *http.Request, match routeMatcher) (dispatch.Request, error) {
if request == nil || match == nil {
return dispatch.Request{}, ErrRouteNotFound
}
host := request.Host
if request.URL != nil && request.URL.Hostname() != "" {
host = request.URL.Hostname()
} else if parsed, _, err := net.SplitHostPort(host); err == nil {
host = parsed
}
path := "/"
if request.URL != nil && request.URL.Path != "" {
path = request.URL.Path
}
headers := make(map[string]string, len(request.Header))
for name := range request.Header {
headers[name] = request.Header.Get(name)
}
matched, ok := match(routing.Request{
Host: strings.ToLower(strings.TrimSuffix(host, ".")),
Method: request.Method,
Path: path,
Headers: headers,
})
if !ok {
return dispatch.Request{}, ErrRouteNotFound
}
switch matched.Action {
case routing.ActionProxy:
return dispatch.Request{
RoutingName: matched.Name, Action: matched.Action, Upstreams: append([]string(nil), matched.Upstreams...),
Strategy: matched.Strategy, OnUnavailable: matched.OnUnavailable, WaitTimeout: matched.WaitTimeout,
}, nil
case routing.ActionReject:
return dispatch.Request{}, fmt.Errorf("%w: %s", ErrRouteRejected, matched.Name)
case routing.ActionDirect:
return dispatch.Request{RoutingName: matched.Name, Action: matched.Action}, nil
default:
return dispatch.Request{}, fmt.Errorf("%w: %s has action %q", ErrRouteRejected, matched.Name, matched.Action)
}
}