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" ) var ( ErrRouteNotFound = errors.New("no gateway routing rule matched") ErrRouteRejected = errors.New("gateway routing rule rejected the request") ErrDirectRouteUnsupported = errors.New("direct gateway routing is not implemented") ) type RulesRouter struct { rules *routing.RuleSet } 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, 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{}, fmt.Errorf("%w: %s", ErrDirectRouteUnsupported, matched.Name) default: return dispatch.Request{}, fmt.Errorf("%w: %s has action %q", ErrRouteRejected, matched.Name, matched.Action) } }