Skip to content

Commit

Permalink
Add glob path matching (#93)
Browse files Browse the repository at this point in the history
Using "glob" matching, a backend server can register for "/foo.*" to
handle routes such as "/foo.bar" and "/foo.bar.baz".

The default option is still prefix matching (original behavior)
  • Loading branch information
dkong authored and magiconair committed May 5, 2016
1 parent 501cc0d commit c183ca0
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 4 deletions.
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type UI struct {

type Proxy struct {
Strategy string
Matcher string
MaxConn int
ShutdownWait time.Duration
DialTimeout time.Duration
Expand Down
1 change: 1 addition & 0 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var Default = &Config{
Proxy: Proxy{
MaxConn: 10000,
Strategy: "rnd",
Matcher: "prefix",
DialTimeout: 30 * time.Second,
LocalIP: LocalIPString(),
},
Expand Down
1 change: 1 addition & 0 deletions config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func fromProperties(p *properties.Properties) (cfg *Config, err error) {
cfg.Proxy = Proxy{
MaxConn: intVal(p, Default.Proxy.MaxConn, "proxy.maxconn"),
Strategy: stringVal(p, Default.Proxy.Strategy, "proxy.strategy"),
Matcher: stringVal(p, Default.Proxy.Matcher, "proxy.matcher"),
ShutdownWait: durationVal(p, Default.Proxy.ShutdownWait, "proxy.shutdownwait"),
DialTimeout: durationVal(p, Default.Proxy.DialTimeout, "proxy.dialtimeout"),
ResponseHeaderTimeout: durationVal(p, Default.Proxy.ResponseHeaderTimeout, "proxy.timeout"),
Expand Down
2 changes: 2 additions & 0 deletions config/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func TestFromProperties(t *testing.T) {
proxy.addr = :1234
proxy.localip = 4.4.4.4
proxy.strategy = rr
proxy.matcher = prefix
proxy.shutdownwait = 500ms
proxy.timeout = 3s
proxy.dialtimeout = 60s
Expand Down Expand Up @@ -50,6 +51,7 @@ ui.title = fabfab
MaxConn: 666,
LocalIP: "4.4.4.4",
Strategy: "rr",
Matcher: "prefix",
ShutdownWait: 500 * time.Millisecond,
DialTimeout: 60 * time.Second,
KeepAliveTimeout: 3 * time.Second,
Expand Down
10 changes: 10 additions & 0 deletions fabio.properties
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@
# proxy.strategy = rnd


# proxy.matcher configures the path matching algorithm.
#
# prefix: prefix matching
# glob: glob matching
#
# The default is
#
# proxy.matcher = prefix


# proxy.shutdownwait configures the time for a graceful shutdown.
#
# After a signal is caught the proxy will immediately suspend
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func newProxy(cfg *config.Config) *proxy.Proxy {
}
log.Printf("[INFO] Using routing strategy %q", cfg.Proxy.Strategy)

if err := route.SetMatcher(cfg.Proxy.Matcher); err != nil {
log.Fatal("[FATAL] ", err)
}
log.Printf("[INFO] Using routing matching %q", cfg.Proxy.Matcher)

tr := &http.Transport{
ResponseHeaderTimeout: cfg.Proxy.ResponseHeaderTimeout,
MaxIdleConnsPerHost: cfg.Proxy.MaxConn,
Expand Down
36 changes: 32 additions & 4 deletions route/matcher.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
package route

import "strings"
import (
"strings"
"path"
"fmt"
"log"
)

// match contains the matcher function
var match matcher = prefixMatcher

// matcher determines whether a host/path matches a route
type matcher func(path string, r *Route) bool
type matcher func(uri string, r *Route) bool

// prefixMatcher matches path to the routes' path.
func prefixMatcher(path string, r *Route) bool {
return strings.HasPrefix(path, r.Path)
func prefixMatcher(uri string, r *Route) bool {
return strings.HasPrefix(uri, r.Path)
}

// globMatcher matches path to the routes' path using globbing.
func globMatcher(uri string, r *Route) bool {
var hasMatch, err = path.Match(r.Path, uri)
if err != nil {
log.Print("[ERROR] Glob matching error %s for path %s route %s", err, uri, r.Path)
return false
}
return hasMatch
}

// SetMatcher sets the matcher function for the proxy.
func SetMatcher(s string) error {
switch s {
case "prefix":
match = prefixMatcher
case "glob":
match = globMatcher
default:
return fmt.Errorf("route: invalid matcher: %s", s)
}
return nil
}
56 changes: 56 additions & 0 deletions route/matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package route

import (
"testing"
)

func TestPrefixMatcher(t *testing.T) {
routeFoo := newRoute("www.example.com", "/foo")

tests := []struct {
uri string
want bool
route *Route
}{
{"/fo", false, routeFoo},
{"/foo", true, routeFoo},
{"/fools", true, routeFoo},
{"/bar", false, routeFoo},
}

for _, tt := range tests {
if got := prefixMatcher(tt.uri, tt.route); got != tt.want {
t.Errorf("%s: got %v want %v", tt.uri, got, tt.want)
}
}
}

func TestGlobMatcher(t *testing.T) {
routeFoo := newRoute("www.example.com", "/foo")
routeFooWild := newRoute("www.example.com", "/foo.*")

tests := []struct {
uri string
want bool
route *Route
}{
{"/fo", false, routeFoo},
{"/foo", true, routeFoo},
{"/fools", false, routeFoo},
{"/bar", false, routeFoo},

{"/fo", false, routeFooWild},
{"/foo", false, routeFooWild},
{"/fools", false, routeFooWild},
{"/foo.", true, routeFooWild},
{"/foo.a", true, routeFooWild},
{"/foo.bar", true, routeFooWild},
{"/foo.bar.baz", true, routeFooWild},
}

for _, tt := range tests {
if got := globMatcher(tt.uri, tt.route); got != tt.want {
t.Errorf("%s: got %v want %v", tt.uri, got, tt.want)
}
}
}

0 comments on commit c183ca0

Please sign in to comment.