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

Commit d5e3795

Browse files
Merge pull request #3 from JSainsburyPLC/feature/redirects
Support redirects
2 parents bf488f0 + 1e899f2 commit d5e3795

File tree

10 files changed

+225
-178
lines changed

10 files changed

+225
-178
lines changed

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ See `examples/config.json`
4444
"type": "proxy", // Required
4545
"path_pattern": "^/test-ui/.*", // regex to match request path. Required
4646
"backend": "http://localhost:3000", // backend scheme and host to proxy to. Required
47-
"rewrite": { // optional rewrite rules
48-
"/test-ui/(.*)": "/$1"
49-
}
47+
"rewrite": [{ // rewrite rules. Optional
48+
"path_pattern": "/test-ui/(.*)",
49+
"to": "/$1",
50+
}],
5051
}
5152
```
5253

@@ -76,6 +77,19 @@ See `examples/config.json`
7677
}
7778
```
7879

80+
### Redirect type rules
81+
82+
```json
83+
{
84+
"type": "redirect",
85+
"path_pattern": "^/test-ui/(.*)",
86+
"redirect": {
87+
"to": "http://localhost:3000/$1",
88+
"type": "temporary"
89+
}
90+
}
91+
```
92+
7993
## Development
8094

8195
### Release

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/http/proxy"
5+
"github.com/JSainsburyPLC/ui-dev-proxy/proxy"
66
"github.com/urfave/cli"
77
"log"
88
"net/url"

domain/config.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,32 @@ import (
77
)
88

99
const (
10-
RouteTypeProxy = "proxy"
11-
RouteTypeMock = "mock"
10+
RouteTypeProxy = "proxy"
11+
RouteTypeMock = "mock"
12+
RouteTypeRedirect = "redirect"
1213
)
1314

1415
type Config struct {
1516
Routes []Route `json:"routes"`
1617
}
1718

1819
type Route struct {
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"`
20+
Type string `json:"type"`
21+
PathPattern *PathPattern `json:"path_pattern"`
22+
Backend *Backend `json:"backend"`
23+
Mock *Mock `json:"mock"`
24+
Rewrite []Rewrite `json:"rewrite"`
25+
Redirect *Redirect `json:"redirect"`
26+
}
27+
28+
type Rewrite struct {
29+
PathPattern *PathPattern `json:"path_pattern"`
30+
To string `json:"to"`
31+
}
32+
33+
type Redirect struct {
34+
To string `json:"to"`
35+
Type string `json:"type"` // either permanent or temporary. Defaults to permanent if not provided
2436
}
2537

2638
type PathPattern struct {

domain/mocks.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func NewMatcher() Matcher {
5454
}
5555
}
5656

57-
// Matches a mock against all matchers
57+
// Match matches a mock against all matchers
5858
func (m Matcher) Match(r *http.Request, mock Mock) bool {
5959
found := true
6060
for _, matcher := range m.matchers {

examples/config.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@
2525
]
2626
}
2727
}
28+
},
29+
{
30+
"type": "proxy",
31+
"path_pattern": "^/test-ui/.*",
32+
"backend": "http://localhost:3000",
33+
"rewrite": [{
34+
"path_pattern": "/test-ui/(.*)",
35+
"to": "/$1"
36+
}]
37+
},
38+
{
39+
"type": "redirect",
40+
"path_pattern": "^/test-ui/(.*)",
41+
"redirect": {
42+
"to": "http://localhost:3000/$1",
43+
"type": "temporary"
44+
}
2845
}
2946
]
3047
}

file/config.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package file
33
import (
44
"encoding/json"
55
"errors"
6-
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
6+
"fmt"
77
"io/ioutil"
88
"os"
99
"path/filepath"
1010
"strings"
11+
12+
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
1113
)
1214

1315
func ConfigProvider() domain.ConfigProvider {
@@ -30,6 +32,13 @@ func ConfigProvider() domain.ConfigProvider {
3032

3133
for _, r := range c.Routes {
3234
if r.Type != domain.RouteTypeMock {
35+
if r.Redirect != nil {
36+
redirectType := r.Redirect.Type
37+
if redirectType != "permanent" && redirectType != "temporary" {
38+
return domain.Config{}, fmt.Errorf("invalid redirect type '%s'", redirectType)
39+
}
40+
}
41+
3342
continue
3443
}
3544

http/rewrite/rewrite.go

Lines changed: 0 additions & 58 deletions
This file was deleted.

http/rewrite/rewrite_test.go

Lines changed: 0 additions & 73 deletions
This file was deleted.

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

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8-
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
9-
"github.com/JSainsburyPLC/ui-dev-proxy/http/rewrite"
10-
118
"log"
129
"net/http"
1310
"net/http/httputil"
1411
"net/url"
12+
"path"
1513
"time"
14+
15+
"github.com/JSainsburyPLC/ui-dev-proxy/domain"
1616
)
1717

1818
const routeCtxKey = "route"
@@ -68,27 +68,19 @@ func director(defaultBackend *url.URL, logger *log.Logger) func(req *http.Reques
6868
req.Host = defaultBackend.Host
6969
return
7070
}
71+
7172
// if route is set redirect to route backend
7273
req.URL.Scheme = route.Backend.Scheme
7374
req.URL.Host = route.Backend.Host
7475
req.Host = route.Backend.Host
7576

7677
// 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 {
78+
for _, rule := range route.Rewrite {
79+
if matches := rule.PathPattern.MatchString(path.Clean(req.URL.Path)); matches {
80+
if err := rewrite(rule, req); err != nil {
81+
logger.Println(fmt.Sprintf("failed to rewrite request. %v", err))
82+
continue
83+
}
9284
break
9385
}
9486
}
@@ -131,6 +123,16 @@ func handler(
131123
logger.Printf("directing to route backend '%s'\n", matchedRoute.Backend.Host)
132124
r = r.WithContext(context.WithValue(r.Context(), routeCtxKey, matchedRoute))
133125
reverseProxy.ServeHTTP(w, r)
126+
case domain.RouteTypeRedirect:
127+
to := replaceURL(matchedRoute.PathPattern, matchedRoute.Redirect.To, r.URL)
128+
u, err := url.Parse(to)
129+
if err != nil {
130+
logger.Printf(err.Error())
131+
w.WriteHeader(http.StatusBadGateway)
132+
_, _ = w.Write([]byte("Bad gateway"))
133+
}
134+
135+
http.Redirect(w, r, u.String(), redirectStatusCode(matchedRoute.Redirect.Type))
134136
case domain.RouteTypeMock:
135137
if !mocksEnabled {
136138
logger.Println("directing to default backend")
@@ -150,6 +152,13 @@ func matchRoute(conf domain.Config, matcher domain.Matcher, r *http.Request, moc
150152
if route.PathPattern.MatchString(r.URL.Path) {
151153
return &route, nil
152154
}
155+
case domain.RouteTypeRedirect:
156+
if route.Redirect == nil {
157+
return nil, errors.New("missing redirect in config")
158+
}
159+
if route.PathPattern.MatchString(r.URL.Path) {
160+
return &route, nil
161+
}
153162
case domain.RouteTypeMock:
154163
if mocksEnabled {
155164
if route.Mock == nil {
@@ -192,3 +201,33 @@ func addCookie(w http.ResponseWriter, cookie domain.Cookie) {
192201
}
193202
http.SetCookie(w, &c)
194203
}
204+
205+
func rewrite(rule domain.Rewrite, req *http.Request) error {
206+
to := path.Clean(replaceURL(rule.PathPattern, rule.To, req.URL))
207+
u, e := url.Parse(to)
208+
if e != nil {
209+
return fmt.Errorf("rewritten URL is not valid. %w", e)
210+
}
211+
212+
req.URL.Path = u.Path
213+
req.URL.RawPath = u.RawPath
214+
if u.RawQuery != "" {
215+
req.URL.RawQuery = u.RawQuery
216+
}
217+
218+
return nil
219+
}
220+
221+
func replaceURL(pattern *domain.PathPattern, to string, u *url.URL) string {
222+
uri := u.RequestURI()
223+
match := pattern.FindStringSubmatchIndex(uri)
224+
result := pattern.ExpandString([]byte(""), to, uri, match)
225+
return string(result[:])
226+
}
227+
228+
func redirectStatusCode(method string) int {
229+
if method == "permanent" || method == "" {
230+
return http.StatusMovedPermanently
231+
}
232+
return http.StatusFound
233+
}

0 commit comments

Comments
 (0)