proxy-pool/internal/gateway/server/routing.go
youfak 1846c98e09
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run
feat: consume gateway routing snapshots
2026-07-31 15:36:56 +08:00

102 lines
3.0 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"
)
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,
}, 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)
}
}