Skip to content

Commit

Permalink
Merge pull request #1 from JSainsburyPLC/feature/rewrite-rules
Browse files Browse the repository at this point in the history
Support nginx style URL rewrites
steinfletcher authored Nov 4, 2019
2 parents ec68010 + b835462 commit 56bc16f
Showing 10 changed files with 194 additions and 19 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
ui-dev-proxy
dist/
.sequence/
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -43,7 +43,10 @@ See `examples/config.json`
{
"type": "proxy", // Required
"path_pattern": "^/test-ui/.*", // regex to match request path. Required
"backend": "http://localhost:3000" // backend scheme and host to proxy to. Required
"backend": "http://localhost:3000", // backend scheme and host to proxy to. Required
"rewrite": { // optional rewrite rules
"/test-ui/(.*)": "/$1"
}
}
```

2 changes: 1 addition & 1 deletion commands/start.go
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ package commands

import (
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
"github.com/JSainsburyPLC/ui-dev-proxy/proxy"
"github.com/JSainsburyPLC/ui-dev-proxy/http/proxy"
"github.com/urfave/cli"
"log"
"net/url"
9 changes: 5 additions & 4 deletions domain/config.go
Original file line number Diff line number Diff line change
@@ -16,10 +16,11 @@ type Config struct {
}

type Route struct {
Type string `json:"type"`
PathPattern *PathPattern `json:"path_pattern"`
Backend *Backend `json:"backend"`
Mock *Mock `json:"mock"`
Type string `json:"type"`
PathPattern *PathPattern `json:"path_pattern"`
Backend *Backend `json:"backend"`
Mock *Mock `json:"mock"`
Rewrite map[string]string `json:"rewrite"`
}

type PathPattern struct {
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
module github.com/JSainsburyPLC/ui-dev-proxy

go 1.12
go 1.13

require (
github.com/steinfletcher/apitest v1.3.8
github.com/stretchr/objx v0.2.0 // indirect
github.com/steinfletcher/apitest v1.3.13
github.com/stretchr/testify v1.4.0
github.com/urfave/cli v1.22.1
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)
10 changes: 3 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -3,24 +3,20 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/steinfletcher/apitest v1.3.8 h1:Q5CrFWbXSo9ocx9pb0IgPw38FKPKfkfEF+3+V35n4M8=
github.com/steinfletcher/apitest v1.3.8/go.mod h1:LOVbGzWvWCiiVE4PZByfhRnA5L00l5uZQEx403xQ4K8=
github.com/steinfletcher/apitest v1.3.13 h1:E0BAXde9dke8jEjK1hqTGvmI20vyyfC+xSdE9nmTc84=
github.com/steinfletcher/apitest v1.3.13/go.mod h1:pCHKMM2TcH1pezw/xbmilaCdK9/dGsoCZBafwaqJ2sY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
26 changes: 24 additions & 2 deletions proxy/proxy.go → http/proxy/proxy.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
"github.com/JSainsburyPLC/ui-dev-proxy/http/rewrite"

"log"
"net/http"
"net/http/httputil"
@@ -30,7 +32,7 @@ func NewProxy(
logger *log.Logger,
) *Proxy {
reverseProxy := &httputil.ReverseProxy{
Director: director(defaultBackend),
Director: director(defaultBackend, logger),
ErrorHandler: errorHandler(logger),
}
return &Proxy{
@@ -56,7 +58,7 @@ func (p *Proxy) Start() {
}
}

func director(defaultBackend *url.URL) func(req *http.Request) {
func director(defaultBackend *url.URL, logger *log.Logger) func(req *http.Request) {
return func(req *http.Request) {
route, ok := req.Context().Value(routeCtxKey).(*domain.Route)
if !ok {
@@ -70,6 +72,26 @@ func director(defaultBackend *url.URL) func(req *http.Request) {
req.URL.Scheme = route.Backend.Scheme
req.URL.Host = route.Backend.Host
req.Host = route.Backend.Host

// apply any defined rewrite rules
for pattern, to := range route.Rewrite {
rule, err := rewrite.NewRule(pattern, to)
if err != nil {
logger.Println(fmt.Sprintf("error creating rewrite rule. %v", err))
continue
}

matched, err := rule.Rewrite(req)
if err != nil {
logger.Println(fmt.Sprintf("failed to rewrite request. %v", err))
continue
}

// recursive rewrites are not supported, exit on first rewrite
if matched {
break
}
}
}
}

23 changes: 23 additions & 0 deletions proxy/proxy_test.go → http/proxy/proxy_test.go
Original file line number Diff line number Diff line change
@@ -61,6 +61,23 @@ func TestProxy_ProxyBackend_UserProxy_Success(t *testing.T) {
End()
}

func TestProxy_ProxyBackend_RewriteURL(t *testing.T) {
newApiTest(configWithRewrite(map[string]string{
"/test-ui/(.*)": "/rewrite-ui/$1",
}), "http://test-backend", false).
Mocks(apitest.NewMock().
Get("http://localhost:3001/rewrite-ui/users/info").
RespondWith().
Status(http.StatusOK).
Body(`{"user_id": "123"}`).
End()).
Get("/test-ui/users/info").
Expect(t).
Status(http.StatusOK).
Body(`{"user_id": "123"}`).
End()
}

func TestProxy_MockBackend_Failure(t *testing.T) {
newApiTest(config(), "http://test-backend", false).
Mocks(
@@ -194,6 +211,12 @@ func config() domain.Config {
}
}

func configWithRewrite(rewrite map[string]string) domain.Config {
conf := config()
conf.Routes[0].Rewrite = rewrite
return conf
}

func invalidTypeConfig() domain.Config {
mockProxyUrlUserUi, err := url.Parse("http://localhost:3001")
if err != nil {
58 changes: 58 additions & 0 deletions http/rewrite/rewrite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package rewrite

import (
"fmt"
"net/http"
"net/url"
"path"
"regexp"
)

type Rule struct {
pattern string
to string
regexp *regexp.Regexp
}

func NewRule(pattern, to string) (Rule, error) {
reg, err := regexp.Compile(pattern)
if err != nil {
return Rule{}, err
}

return Rule{
pattern: pattern,
to: to,
regexp: reg,
}, nil
}

func (r *Rule) Rewrite(req *http.Request) (bool, error) {
oriPath := req.URL.Path

if !r.regexp.MatchString(oriPath) {
return false, nil
}

to := path.Clean(r.Replace(req.URL))
u, e := url.Parse(to)
if e != nil {
return false, fmt.Errorf("rewritten URL is not valid. %w", e)
}

req.URL.Path = u.Path
req.URL.RawPath = u.RawPath
if u.RawQuery != "" {
req.URL.RawQuery = u.RawQuery
}

return true, nil
}

func (r *Rule) Replace(u *url.URL) string {
uri := u.RequestURI()
patternRegexp := regexp.MustCompile(r.pattern)
match := patternRegexp.FindStringSubmatchIndex(uri)
result := patternRegexp.ExpandString([]byte(""), r.to, uri, match)
return string(result[:])
}
73 changes: 73 additions & 0 deletions http/rewrite/rewrite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package rewrite_test

import (
"net/http"
"testing"

"github.com/JSainsburyPLC/ui-dev-proxy/http/rewrite"
"github.com/stretchr/testify/assert"
)

func TestRewrite(t *testing.T) {
tests := map[string]struct {
pattern string
to string
before string
after string
matched bool
}{
"constant": {
pattern: "/a",
to: "/b",
before: "/a",
after: "/b",
matched: true,
},
"preserves original URL if no match": {
pattern: "/a",
to: "/b",
before: "/c",
after: "/c",
matched: false,
},
"match group": {
pattern: "/api/(.*)",
to: "/$1",
before: "/api/my-endpoint",
after: "/my-endpoint",
matched: true,
},
"multiple match groups": {
pattern: "/a/(.*)/b/(.*)",
to: "/x/y/$1/z/$2",
before: "/a/oo/b/qq",
after: "/x/y/oo/z/qq",
matched: true,
},
"encoded characters": {
pattern: "/a/(.*)",
to: "/b/$1",
before: "/a/x-1%2F",
after: "/b/x-1%2F",
matched: true,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
req, err := http.NewRequest("GET", test.before, nil)
if err != nil {
t.Fatalf("failed to create request %v %v", test, err)
}
rule, err := rewrite.NewRule(test.pattern, test.to)
if err != nil {
t.Fatal(err)
}

matched, err := rule.Rewrite(req)

assert.NoError(t, err)
assert.Equal(t, test.after, req.URL.EscapedPath())
assert.Equal(t, test.matched, matched)
})
}
}

0 comments on commit 56bc16f

Please sign in to comment.