mirror of
https://github.com/umputun/reproxy.git
synced 2024-08-16 12:10:31 +03:00
Support regex in host / server
Main consideration is backward compatibility. example.com should be treated as an exact match, where possible. So current order is: exact host, regex host, * or "". Regex matches are cached for better performance, cache is invalidated once mappings are refreshed.
This commit is contained in:
parent
1783f540f8
commit
184d5ba87c
@ -30,7 +30,7 @@ Reproxy is a simple edge HTTP(s) server / reverse proxy supporting various provi
|
||||
|
||||
</div>
|
||||
|
||||
Server (host) can be set as FQDN, i.e. `s.example.com` or `*` (catch all). Requested url can be regex, for example `^/api/(.*)` and destination url may have regex matched groups in, i.e. `http://d.example.com:8080/$1`. For the example above `http://s.example.com/api/something?foo=bar` will be proxied to `http://d.example.com:8080/something?foo=bar`.
|
||||
Server (host) can be set as FQDN, i.e. `s.example.com`, `*` (catch all) or a regex. Exact match takes priority, so if there are two rules with servers `example.com` and `example\.(com|org)`, request to `example.com/some/url` will match the former. Requested url can be regex, for example `^/api/(.*)` and destination url may have regex matched groups in, i.e. `http://d.example.com:8080/$1`. For the example above `http://s.example.com/api/something?foo=bar` will be proxied to `http://d.example.com:8080/something?foo=bar`.
|
||||
|
||||
For convenience, requests with the trailing `/` and without regex groups expanded to `/(.*)`, and destinations in those cases expanded to `/$1`. I.e. `/api/` -> `http://127.0.0.1/service` will be translated to `^/api/(.*)` -> `http://127.0.0.1/service/$1`
|
||||
|
||||
|
@ -21,10 +21,11 @@ import (
|
||||
|
||||
// Service implements discovery with multiple providers and url matcher
|
||||
type Service struct {
|
||||
providers []Provider
|
||||
mappers map[string][]URLMapper
|
||||
lock sync.RWMutex
|
||||
interval time.Duration
|
||||
providers []Provider
|
||||
mappers map[string][]URLMapper
|
||||
mappersCache map[string][]URLMapper
|
||||
lock sync.RWMutex
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
// URLMapper contains all info about source and destination routes
|
||||
@ -144,6 +145,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
}
|
||||
s.lock.Lock()
|
||||
s.mappers = make(map[string][]URLMapper)
|
||||
s.mappersCache = make(map[string][]URLMapper)
|
||||
for _, m := range lst {
|
||||
s.mappers[m.Server] = append(s.mappers[m.Server], m)
|
||||
}
|
||||
@ -161,7 +163,7 @@ func (s *Service) Match(srv, src string) (res Matches) {
|
||||
|
||||
lastSrcMatch := ""
|
||||
for _, srvName := range []string{srv, "*", ""} {
|
||||
for _, m := range s.mappers[srvName] {
|
||||
for _, m := range findMatchingMappers(s, srvName) {
|
||||
|
||||
// if the first match found and the next src match is not identical we can stop as src match regexes presorted
|
||||
if len(res.Routes) > 0 && m.SrcMatch.String() != lastSrcMatch {
|
||||
@ -198,6 +200,37 @@ func (s *Service) Match(srv, src string) (res Matches) {
|
||||
return res
|
||||
}
|
||||
|
||||
func findMatchingMappers(s *Service, srvName string) []URLMapper {
|
||||
// strict match - for backward compatibility
|
||||
if mappers, isStrictMatch := s.mappers[srvName]; isStrictMatch {
|
||||
return mappers
|
||||
}
|
||||
|
||||
if cachedMapper, isCached := s.mappersCache[srvName]; isCached {
|
||||
return cachedMapper
|
||||
}
|
||||
|
||||
for mapperServer, mapper := range s.mappers {
|
||||
// * and "" should not be treated as regex and require exact match (above)
|
||||
if mapperServer == "*" || mapperServer == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(mapperServer)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] invalid regexp %s: %s", mapperServer, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if re.MatchString(srvName) {
|
||||
s.mappersCache[srvName] = mapper
|
||||
return mapper
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScheduleHealthCheck starts background loop with health-check
|
||||
func (s *Service) ScheduleHealthCheck(ctx context.Context, interval time.Duration) {
|
||||
log.Printf("health-check scheduled every %s", interval)
|
||||
|
@ -161,7 +161,7 @@ func TestService_Match(t *testing.T) {
|
||||
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
t.Run(strconv.Itoa(i)+"-"+tt.server, func(t *testing.T) {
|
||||
res := svc.Match(tt.server, tt.src)
|
||||
require.Equal(t, len(tt.res.Routes), len(res.Routes), res.Routes)
|
||||
for i := 0; i < len(res.Routes); i++ {
|
||||
@ -173,6 +173,108 @@ func TestService_Match(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_MatchServerRegex(t *testing.T) {
|
||||
mockProvider := &ProviderMock{
|
||||
EventsFunc: func(ctx context.Context) <-chan ProviderID {
|
||||
res := make(chan ProviderID, 1)
|
||||
res <- PIFile
|
||||
return res
|
||||
},
|
||||
ListFunc: func() ([]URLMapper, error) {
|
||||
return []URLMapper{
|
||||
// invalid regex
|
||||
{Server: "[", SrcMatch: *regexp.MustCompile("^/"),
|
||||
Dst: "http://127.0.0.10:8080/", MatchType: MTProxy, dead: false},
|
||||
|
||||
// regex servers
|
||||
{Server: "test-prefix\\.(.*)", SrcMatch: *regexp.MustCompile("^/"),
|
||||
Dst: "http://127.0.0.1:8080/", MatchType: MTProxy, dead: false},
|
||||
{Server: "(.*)\\.test-domain\\.(com|org)", SrcMatch: *regexp.MustCompile("^/"),
|
||||
Dst: "http://127.0.0.2:8080/", MatchType: MTProxy, dead: false},
|
||||
|
||||
// strict match
|
||||
{Server: "test-prefix.exact.com", SrcMatch: *regexp.MustCompile("/"),
|
||||
Dst: "http://127.0.0.4:8080", MatchType: MTProxy, dead: false},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
svc := NewService([]Provider{mockProvider}, time.Millisecond*100)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := svc.Run(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, context.DeadlineExceeded, err)
|
||||
|
||||
tbl := []struct {
|
||||
server, src string
|
||||
res Matches
|
||||
}{
|
||||
// strict match should take priority
|
||||
{"test-prefix.exact.com", "/", Matches{MTProxy, []MatchedRoute{{Destination: "http://127.0.0.4:8080/", Alive: true}}}},
|
||||
|
||||
// regex servers
|
||||
{"test-prefix.example.com", "/", Matches{MTProxy, []MatchedRoute{{Destination: "http://127.0.0.1:8080/", Alive: true}}}},
|
||||
{"another-prefix.example.com", "/", Matches{MTProxy, nil}},
|
||||
{"another-prefix.test-domain.org", "/", Matches{MTProxy, []MatchedRoute{{Destination: "http://127.0.0.2:8080/", Alive: true}}}},
|
||||
{"another-prefix.test-domain.net", "/", Matches{MTProxy, nil}},
|
||||
}
|
||||
|
||||
for i, tt := range tbl {
|
||||
tt := tt
|
||||
t.Run(strconv.Itoa(i)+"-"+tt.server, func(t *testing.T) {
|
||||
res := svc.Match(tt.server, tt.src)
|
||||
require.Equal(t, len(tt.res.Routes), len(res.Routes), res.Routes)
|
||||
for i := 0; i < len(res.Routes); i++ {
|
||||
assert.Equal(t, tt.res.Routes[i].Alive, res.Routes[i].Alive)
|
||||
assert.Equal(t, tt.res.Routes[i].Destination, res.Routes[i].Destination)
|
||||
}
|
||||
assert.Equal(t, tt.res.MatchType, res.MatchType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_MatchServerRegexInvalidateCache(t *testing.T) {
|
||||
res := make(chan ProviderID)
|
||||
serverRegex := "test-(.*)"
|
||||
p1 := &ProviderMock{
|
||||
EventsFunc: func(ctx context.Context) <-chan ProviderID {
|
||||
return res
|
||||
},
|
||||
ListFunc: func() ([]URLMapper, error) {
|
||||
return []URLMapper{
|
||||
{Server: serverRegex, SrcMatch: *regexp.MustCompile("^/"), Dst: "http://127.0.0.1/foo"},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewService([]Provider{p1}, time.Millisecond*10)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
err := svc.Run(ctx)
|
||||
require.Error(t, err)
|
||||
}()
|
||||
|
||||
res <- PIFile
|
||||
|
||||
// wait for update
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
match := svc.Match("test-server", "/")
|
||||
assert.Len(t, match.Routes, 1)
|
||||
|
||||
serverRegex = "another-(.*)"
|
||||
res <- PIFile
|
||||
|
||||
// wait for cache invalidation
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
match = svc.Match("test-server", "/")
|
||||
assert.Len(t, match.Routes, 0)
|
||||
}
|
||||
|
||||
func TestService_MatchConflictRegex(t *testing.T) {
|
||||
p1 := &ProviderMock{
|
||||
EventsFunc: func(ctx context.Context) <-chan ProviderID {
|
||||
|
Loading…
Reference in New Issue
Block a user