mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-09-17 21:52:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6878099b | ||
|
|
0d8b136b91 |
@@ -26,6 +26,17 @@ type Rule struct {
|
||||
// The incoming request path and query are forwarded unchanged; only the
|
||||
// scheme and host are rewritten to Target's.
|
||||
Target string
|
||||
|
||||
// Exclude is a list of URL path prefixes that this rule should not
|
||||
// proxy, even though they fall under URLPrefix. Each entry is a full
|
||||
// path from root and must itself start with URLPrefix (e.g. rule
|
||||
// URLPrefix "/api" excluding a subpath must use "/api/health", not
|
||||
// "/health"). A request matching an Exclude prefix is treated as if
|
||||
// this rule didn't match at all: matching continues against any other
|
||||
// configured rule, falling back if none match. This is typically used
|
||||
// to carve out paths (e.g. "/health") from a catch-all "/" rule so
|
||||
// they're served by the fallback handler instead of being proxied.
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
// DefaultTimeout is the dial and response-header timeout applied to
|
||||
@@ -50,8 +61,19 @@ func WithTimeout(d time.Duration) Option {
|
||||
|
||||
// compiledRule pairs a Rule with its ready-to-use reverse proxy.
|
||||
type compiledRule struct {
|
||||
prefix string
|
||||
proxy *httputil.ReverseProxy
|
||||
prefix string
|
||||
excludes []string
|
||||
proxy *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
// excluded reports whether path falls under one of the rule's Exclude prefixes.
|
||||
func (r *compiledRule) excluded(path string) bool {
|
||||
for _, ex := range r.excludes {
|
||||
if strings.HasPrefix(path, ex) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Service holds a compiled set of proxy rules and performs longest-prefix
|
||||
@@ -98,9 +120,19 @@ func NewService(rules []Rule, opts ...Option) (*Service, error) {
|
||||
return nil, fmt.Errorf("quickproxy: invalid target %q for prefix %q", r.Target, r.URLPrefix)
|
||||
}
|
||||
|
||||
for _, ex := range r.Exclude {
|
||||
if !strings.HasPrefix(ex, "/") {
|
||||
return nil, fmt.Errorf("quickproxy: exclude prefix %q for rule %q must start with /", ex, r.URLPrefix)
|
||||
}
|
||||
if !strings.HasPrefix(ex, r.URLPrefix) {
|
||||
return nil, fmt.Errorf("quickproxy: exclude prefix %q for rule %q must itself start with the rule's URLPrefix", ex, r.URLPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
compiled = append(compiled, compiledRule{
|
||||
prefix: r.URLPrefix,
|
||||
proxy: newReverseProxy(target, cfg.timeout),
|
||||
prefix: r.URLPrefix,
|
||||
excludes: r.Exclude,
|
||||
proxy: newReverseProxy(target, cfg.timeout),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -174,11 +206,17 @@ func (s *Service) Handler(fallback http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
// match returns the longest-prefix rule matching path, or nil if none match.
|
||||
// A rule whose Exclude covers path is skipped, and matching continues
|
||||
// against the next-longest-prefix rule.
|
||||
func (s *Service) match(path string) *compiledRule {
|
||||
for i := range s.rules {
|
||||
if strings.HasPrefix(path, s.rules[i].prefix) {
|
||||
return &s.rules[i]
|
||||
if !strings.HasPrefix(path, s.rules[i].prefix) {
|
||||
continue
|
||||
}
|
||||
if s.rules[i].excluded(path) {
|
||||
continue
|
||||
}
|
||||
return &s.rules[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,19 @@ func TestNewService_Validation(t *testing.T) {
|
||||
{URLPrefix: "/api", Target: "http://localhost:1"},
|
||||
{URLPrefix: "/api", Target: "http://localhost:2"},
|
||||
}, true},
|
||||
{"bad exclude prefix", []Rule{
|
||||
{URLPrefix: "/", Target: "http://localhost:1", Exclude: []string{"health"}},
|
||||
}, true},
|
||||
{"exclude outside rule's URLPrefix", []Rule{
|
||||
{URLPrefix: "/api", Target: "http://localhost:1", Exclude: []string{"/health"}},
|
||||
}, true},
|
||||
{"valid", []Rule{{URLPrefix: "/api", Target: "http://localhost:1"}}, false},
|
||||
{"valid with exclude", []Rule{
|
||||
{URLPrefix: "/", Target: "http://localhost:1", Exclude: []string{"/health"}},
|
||||
}, false},
|
||||
{"valid with nested exclude", []Rule{
|
||||
{URLPrefix: "/api", Target: "http://localhost:1", Exclude: []string{"/api/health"}},
|
||||
}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -186,6 +198,81 @@ func TestHandler_LongestPrefixMatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_ExcludeFallsBackToFallback(t *testing.T) {
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("upstream:" + r.URL.Path))
|
||||
}))
|
||||
defer upstream.Close()
|
||||
|
||||
svc, err := NewService([]Rule{
|
||||
{URLPrefix: "/", Target: upstream.URL, Exclude: []string{"/health"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
|
||||
handler := svc.Handler(fallbackHandler("fallback-content"))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if got := rr.Body.String(); got != "fallback-content" {
|
||||
t.Fatalf("body = %q, want fallback-content", got)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/health/live", nil)
|
||||
rr = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if got := rr.Body.String(); got != "fallback-content" {
|
||||
t.Fatalf("body = %q, want fallback-content", got)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/other", nil)
|
||||
rr = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if got := rr.Body.String(); got != "upstream:/other" {
|
||||
t.Fatalf("body = %q, want upstream:/other", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_ExcludeFallsThroughToNextRule(t *testing.T) {
|
||||
specific := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("specific"))
|
||||
}))
|
||||
defer specific.Close()
|
||||
|
||||
general := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("general"))
|
||||
}))
|
||||
defer general.Close()
|
||||
|
||||
svc, err := NewService([]Rule{
|
||||
{URLPrefix: "/api", Target: general.URL},
|
||||
{URLPrefix: "/api/v1", Target: specific.URL, Exclude: []string{"/api/v1/health"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
|
||||
handler := svc.Handler(fallbackHandler("fallback"))
|
||||
|
||||
for path, want := range map[string]string{
|
||||
"/api/v1/thing": "specific",
|
||||
"/api/v1/health": "general",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||
rr := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if got := rr.Body.String(); got != want {
|
||||
t.Errorf("path %s: body = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandler_NoMatchFallsBack(t *testing.T) {
|
||||
svc, err := NewService([]Rule{{URLPrefix: "/api", Target: "http://127.0.0.1:1"}})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user