Skip to content
This repository was archived by the owner on Feb 17, 2025. It is now read-only.

Commit 56bc16f

Browse files
Merge pull request #1 from JSainsburyPLC/feature/rewrite-rules
Support nginx style URL rewrites
2 parents ec68010 + b835462 commit 56bc16f

File tree

10 files changed

+194
-19
lines changed

10 files changed

+194
-19
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
ui-dev-proxy
22
dist/
3+
.sequence/

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ See `examples/config.json`
4343
{
4444
"type": "proxy", // Required
4545
"path_pattern": "^/test-ui/.*", // regex to match request path. Required
46-
"backend": "http://localhost:3000" // backend scheme and host to proxy to. Required
46+
"backend": "http://localhost:3000", // backend scheme and host to proxy to. Required
47+
"rewrite": { // optional rewrite rules
48+
"/test-ui/(.*)": "/$1"
49+
}
4750
}
4851
```
4952

commands/start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package commands
22

33
import (
44
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
5-
"github.com/JSainsburyPLC/ui-dev-proxy/proxy"
5+
"github.com/JSainsburyPLC/ui-dev-proxy/http/proxy"
66
"github.com/urfave/cli"
77
"log"
88
"net/url"

domain/config.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ type Config struct {
1616
}
1717

1818
type Route struct {
19-
Type string `json:"type"`
20-
PathPattern *PathPattern `json:"path_pattern"`
21-
Backend *Backend `json:"backend"`
22-
Mock *Mock `json:"mock"`
19+
Type string `json:"type"`
20+
PathPattern *PathPattern `json:"path_pattern"`
21+
Backend *Backend `json:"backend"`
22+
Mock *Mock `json:"mock"`
23+
Rewrite map[string]string `json:"rewrite"`
2324
}
2425

2526
type PathPattern struct {

go.mod

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
module github.com/JSainsburyPLC/ui-dev-proxy
22

3-
go 1.12
3+
go 1.13
44

55
require (
6-
github.com/steinfletcher/apitest v1.3.8
7-
github.com/stretchr/objx v0.2.0 // indirect
6+
github.com/steinfletcher/apitest v1.3.13
87
github.com/stretchr/testify v1.4.0
98
github.com/urfave/cli v1.22.1
10-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
119
)

go.sum

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,20 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY
33
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
44
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
55
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
7-
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
86
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
97
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
108
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
119
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
1210
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
1311
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
14-
github.com/steinfletcher/apitest v1.3.8 h1:Q5CrFWbXSo9ocx9pb0IgPw38FKPKfkfEF+3+V35n4M8=
15-
github.com/steinfletcher/apitest v1.3.8/go.mod h1:LOVbGzWvWCiiVE4PZByfhRnA5L00l5uZQEx403xQ4K8=
12+
github.com/steinfletcher/apitest v1.3.13 h1:E0BAXde9dke8jEjK1hqTGvmI20vyyfC+xSdE9nmTc84=
13+
github.com/steinfletcher/apitest v1.3.13/go.mod h1:pCHKMM2TcH1pezw/xbmilaCdK9/dGsoCZBafwaqJ2sY=
1614
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
17-
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
18-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
1915
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
2016
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
2117
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
2218
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
19+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2320
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
24-
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2521
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
2622
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

proxy/proxy.go renamed to http/proxy/proxy.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"errors"
77
"fmt"
88
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
9+
"github.com/JSainsburyPLC/ui-dev-proxy/http/rewrite"
10+
911
"log"
1012
"net/http"
1113
"net/http/httputil"
@@ -30,7 +32,7 @@ func NewProxy(
3032
logger *log.Logger,
3133
) *Proxy {
3234
reverseProxy := &httputil.ReverseProxy{
33-
Director: director(defaultBackend),
35+
Director: director(defaultBackend, logger),
3436
ErrorHandler: errorHandler(logger),
3537
}
3638
return &Proxy{
@@ -56,7 +58,7 @@ func (p *Proxy) Start() {
5658
}
5759
}
5860

59-
func director(defaultBackend *url.URL) func(req *http.Request) {
61+
func director(defaultBackend *url.URL, logger *log.Logger) func(req *http.Request) {
6062
return func(req *http.Request) {
6163
route, ok := req.Context().Value(routeCtxKey).(*domain.Route)
6264
if !ok {
@@ -70,6 +72,26 @@ func director(defaultBackend *url.URL) func(req *http.Request) {
7072
req.URL.Scheme = route.Backend.Scheme
7173
req.URL.Host = route.Backend.Host
7274
req.Host = route.Backend.Host
75+
76+
// apply any defined rewrite rules
77+
for pattern, to := range route.Rewrite {
78+
rule, err := rewrite.NewRule(pattern, to)
79+
if err != nil {
80+
logger.Println(fmt.Sprintf("error creating rewrite rule. %v", err))
81+
continue
82+
}
83+
84+
matched, err := rule.Rewrite(req)
85+
if err != nil {
86+
logger.Println(fmt.Sprintf("failed to rewrite request. %v", err))
87+
continue
88+
}
89+
90+
// recursive rewrites are not supported, exit on first rewrite
91+
if matched {
92+
break
93+
}
94+
}
7395
}
7496
}
7597

proxy/proxy_test.go renamed to http/proxy/proxy_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ func TestProxy_ProxyBackend_UserProxy_Success(t *testing.T) {
6161
End()
6262
}
6363

64+
func TestProxy_ProxyBackend_RewriteURL(t *testing.T) {
65+
newApiTest(configWithRewrite(map[string]string{
66+
"/test-ui/(.*)": "/rewrite-ui/$1",
67+
}), "http://test-backend", false).
68+
Mocks(apitest.NewMock().
69+
Get("http://localhost:3001/rewrite-ui/users/info").
70+
RespondWith().
71+
Status(http.StatusOK).
72+
Body(`{"user_id": "123"}`).
73+
End()).
74+
Get("/test-ui/users/info").
75+
Expect(t).
76+
Status(http.StatusOK).
77+
Body(`{"user_id": "123"}`).
78+
End()
79+
}
80+
6481
func TestProxy_MockBackend_Failure(t *testing.T) {
6582
newApiTest(config(), "http://test-backend", false).
6683
Mocks(
@@ -194,6 +211,12 @@ func config() domain.Config {
194211
}
195212
}
196213

214+
func configWithRewrite(rewrite map[string]string) domain.Config {
215+
conf := config()
216+
conf.Routes[0].Rewrite = rewrite
217+
return conf
218+
}
219+
197220
func invalidTypeConfig() domain.Config {
198221
mockProxyUrlUserUi, err := url.Parse("http://localhost:3001")
199222
if err != nil {

http/rewrite/rewrite.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package rewrite
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/url"
7+
"path"
8+
"regexp"
9+
)
10+
11+
type Rule struct {
12+
pattern string
13+
to string
14+
regexp *regexp.Regexp
15+
}
16+
17+
func NewRule(pattern, to string) (Rule, error) {
18+
reg, err := regexp.Compile(pattern)
19+
if err != nil {
20+
return Rule{}, err
21+
}
22+
23+
return Rule{
24+
pattern: pattern,
25+
to: to,
26+
regexp: reg,
27+
}, nil
28+
}
29+
30+
func (r *Rule) Rewrite(req *http.Request) (bool, error) {
31+
oriPath := req.URL.Path
32+
33+
if !r.regexp.MatchString(oriPath) {
34+
return false, nil
35+
}
36+
37+
to := path.Clean(r.Replace(req.URL))
38+
u, e := url.Parse(to)
39+
if e != nil {
40+
return false, fmt.Errorf("rewritten URL is not valid. %w", e)
41+
}
42+
43+
req.URL.Path = u.Path
44+
req.URL.RawPath = u.RawPath
45+
if u.RawQuery != "" {
46+
req.URL.RawQuery = u.RawQuery
47+
}
48+
49+
return true, nil
50+
}
51+
52+
func (r *Rule) Replace(u *url.URL) string {
53+
uri := u.RequestURI()
54+
patternRegexp := regexp.MustCompile(r.pattern)
55+
match := patternRegexp.FindStringSubmatchIndex(uri)
56+
result := patternRegexp.ExpandString([]byte(""), r.to, uri, match)
57+
return string(result[:])
58+
}

http/rewrite/rewrite_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package rewrite_test
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/JSainsburyPLC/ui-dev-proxy/http/rewrite"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestRewrite(t *testing.T) {
12+
tests := map[string]struct {
13+
pattern string
14+
to string
15+
before string
16+
after string
17+
matched bool
18+
}{
19+
"constant": {
20+
pattern: "/a",
21+
to: "/b",
22+
before: "/a",
23+
after: "/b",
24+
matched: true,
25+
},
26+
"preserves original URL if no match": {
27+
pattern: "/a",
28+
to: "/b",
29+
before: "/c",
30+
after: "/c",
31+
matched: false,
32+
},
33+
"match group": {
34+
pattern: "/api/(.*)",
35+
to: "/$1",
36+
before: "/api/my-endpoint",
37+
after: "/my-endpoint",
38+
matched: true,
39+
},
40+
"multiple match groups": {
41+
pattern: "/a/(.*)/b/(.*)",
42+
to: "/x/y/$1/z/$2",
43+
before: "/a/oo/b/qq",
44+
after: "/x/y/oo/z/qq",
45+
matched: true,
46+
},
47+
"encoded characters": {
48+
pattern: "/a/(.*)",
49+
to: "/b/$1",
50+
before: "/a/x-1%2F",
51+
after: "/b/x-1%2F",
52+
matched: true,
53+
},
54+
}
55+
for name, test := range tests {
56+
t.Run(name, func(t *testing.T) {
57+
req, err := http.NewRequest("GET", test.before, nil)
58+
if err != nil {
59+
t.Fatalf("failed to create request %v %v", test, err)
60+
}
61+
rule, err := rewrite.NewRule(test.pattern, test.to)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
66+
matched, err := rule.Rewrite(req)
67+
68+
assert.NoError(t, err)
69+
assert.Equal(t, test.after, req.URL.EscapedPath())
70+
assert.Equal(t, test.matched, matched)
71+
})
72+
}
73+
}

0 commit comments

Comments
 (0)