Redirect (#87)

* add @code redirect prefix

* add proxy handling for redirects #86

* add info about redirects
This commit is contained in:
Umputun 2021-06-06 18:13:59 -05:00 committed by GitHub
parent 680d988d42
commit aea74d717f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 182 additions and 24 deletions

View File

@ -170,6 +170,15 @@ There are two ways to set cache duration:
1. A single value for all static assets. This is as simple as `--assets.cache=48h`.
2. Custom duration for different mime types. It should include two parts - the default value and the pairs of mime:duration. In command line this looks like multiple `--assets.cache` options, i.e. `--assets.cache=48h --assets.cache=text/html:24h --assets.cache=image/png:2h`. Environment values should be comma-separated, i.e. `ASSETS_CACHE=48h,text/html:24h,image/png:2h`
## Redirects
By default reproxy treats destination as a proxy location, i.e. it invokes http call internally and returns response back to the client. However by prefixing destination url with `@code` this behaviour can be changed to a permanent (status code 301) or temporary (status code 302) redirects. I.e. destination set to `@301 https://example.com/something` with cause permanent http redirect to `Location: https://example.com/something`
supported codes:
- `@301`, `@perm` - permanent redirect
- `@302`, `@temp`, `@tmp` - temporary redirect
## More options
- `--gzip` enables gzip compression for responses.

View File

@ -29,12 +29,13 @@ type Service struct {
// URLMapper contains all info about source and destination routes
type URLMapper struct {
Server string
SrcMatch regexp.Regexp
Dst string
ProviderID ProviderID
PingURL string
MatchType MatchType
Server string
SrcMatch regexp.Regexp
Dst string
ProviderID ProviderID
PingURL string
MatchType MatchType
RedirectType RedirectType
AssetsLocation string
AssetsWebRoot string
@ -92,6 +93,16 @@ func (m MatchType) String() string {
}
}
// RedirectType defines types of redirects
type RedirectType int
// enum of all redirect types
const (
RTNone RedirectType = 0
RTPerm RedirectType = 301
RTTemp RedirectType = 302
)
// NewService makes service with given providers
func NewService(providers []Provider, interval time.Duration) *Service {
return &Service{providers: providers, interval: interval}
@ -306,6 +317,7 @@ func (s *Service) mergeLists() (res []URLMapper) {
continue
}
for i := range lst {
lst[i] = s.redirects(lst[i])
lst[i] = s.extendMapper(lst[i])
}
res = append(res, lst...)
@ -362,6 +374,27 @@ func (s *Service) extendMapper(m URLMapper) URLMapper {
return res
}
// redirects process @code prefix and sets redirect type
func (s *Service) redirects(m URLMapper) URLMapper {
switch {
case strings.HasPrefix(m.Dst, "@301 ") && len(m.Dst) > 4:
m.Dst = m.Dst[5:]
m.RedirectType = RTPerm
case strings.HasPrefix(m.Dst, "@perm ") && len(m.Dst) > 5:
m.Dst = m.Dst[6:]
m.RedirectType = RTPerm
case (strings.HasPrefix(m.Dst, "@302 ") || strings.HasPrefix(m.Dst, "@tmp ")) && len(m.Dst) > 4:
m.Dst = m.Dst[5:]
m.RedirectType = RTTemp
case strings.HasPrefix(m.Dst, "@temp ") && len(m.Dst) > 5:
m.Dst = m.Dst[6:]
m.RedirectType = RTTemp
default:
m.RedirectType = RTNone
}
return m
}
func (s *Service) mergeEvents(ctx context.Context, chs ...<-chan ProviderID) <-chan ProviderID {
var wg sync.WaitGroup
out := make(chan ProviderID)

View File

@ -248,6 +248,56 @@ func TestService_extendRule(t *testing.T) {
}
func TestService_redirects(t *testing.T) {
tbl := []struct {
inp URLMapper
out URLMapper
}{
{
URLMapper{Dst: "/blah"},
URLMapper{Dst: "/blah", RedirectType: RTNone},
},
{
URLMapper{Dst: "http://example.com/blah"},
URLMapper{Dst: "http://example.com/blah", RedirectType: RTNone},
},
{
URLMapper{Dst: "@301 http://example.com/blah"},
URLMapper{Dst: "http://example.com/blah", RedirectType: RTPerm},
},
{
URLMapper{Dst: "@perm http://example.com/blah"},
URLMapper{Dst: "http://example.com/blah", RedirectType: RTPerm},
},
{
URLMapper{Dst: "@302 http://example.com/blah"},
URLMapper{Dst: "http://example.com/blah", RedirectType: RTTemp},
},
{
URLMapper{Dst: "@tmp http://example.com/blah"},
URLMapper{Dst: "http://example.com/blah", RedirectType: RTTemp},
},
{
URLMapper{Dst: "@temp http://example.com/blah"},
URLMapper{Dst: "http://example.com/blah", RedirectType: RTTemp},
},
{
URLMapper{Dst: "@blah http://example.com/blah"},
URLMapper{Dst: "@blah http://example.com/blah", RedirectType: RTNone},
},
}
svc := &Service{}
for i, tt := range tbl {
tt := tt
t.Run(strconv.Itoa(i), func(t *testing.T) {
res := svc.redirects(tt.inp)
assert.Equal(t, tt.out, res)
})
}
}
func TestService_ScheduleHealthCheck(t *testing.T) {
randomPort := rand.Intn(10000) + 40000

View File

@ -228,9 +228,19 @@ func (h *Http) proxyHandler() http.HandlerFunc {
switch matchType {
case discovery.MTProxy:
uu := r.Context().Value(ctxURL).(*url.URL)
log.Printf("[DEBUG] proxy to %s", uu)
reverseProxy.ServeHTTP(w, r)
switch match.Mapper.RedirectType {
case discovery.RTNone:
uu := r.Context().Value(ctxURL).(*url.URL)
log.Printf("[DEBUG] proxy to %s", uu)
reverseProxy.ServeHTTP(w, r)
case discovery.RTPerm:
log.Printf("[DEBUG] redirect (301) to %s", match.Destination)
http.Redirect(w, r, match.Destination, http.StatusMovedPermanently)
case discovery.RTTemp:
log.Printf("[DEBUG] redirect (302) to %s", match.Destination)
http.Redirect(w, r, match.Destination, http.StatusFound)
}
case discovery.MTStatic:
// static match result has webroot:location, i.e. /www:/var/somedir/
ae := strings.Split(match.Destination, ":")

View File

@ -221,21 +221,21 @@ func TestHttp_DoWithAssetRules(t *testing.T) {
client := http.Client{}
// {
// req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/api/something", nil)
// require.NoError(t, err)
// resp, err := client.Do(req)
// require.NoError(t, err)
// defer resp.Body.Close()
// assert.Equal(t, http.StatusOK, resp.StatusCode)
// t.Logf("%+v", resp.Header)
//
// body, err := io.ReadAll(resp.Body)
// require.NoError(t, err)
// assert.Equal(t, "response /567/something", string(body))
// assert.Equal(t, "", resp.Header.Get("App-Method"))
// assert.Equal(t, "v1", resp.Header.Get("h1"))
// }
{
req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/api/something", nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
t.Logf("%+v", resp.Header)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
assert.Equal(t, "response /567/something", string(body))
assert.Equal(t, "", resp.Header.Get("App-Method"))
assert.Equal(t, "v1", resp.Header.Get("h1"))
}
{
resp, err := client.Get("http://localhost:" + strconv.Itoa(port) + "/web/1.html")
@ -253,6 +253,62 @@ func TestHttp_DoWithAssetRules(t *testing.T) {
}
}
func TestHttp_DoWithRedirects(t *testing.T) {
port := rand.Intn(10000) + 40000
cc := NewCacheControl(time.Hour * 12)
h := Http{Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond}, Address: fmt.Sprintf("127.0.0.1:%d", port),
AccessLog: io.Discard, CacheControl: cc, Reporter: &ErrorReporter{}}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
svc := discovery.NewService([]discovery.Provider{
&provider.Static{Rules: []string{
"localhost,^/api/(.*),@perm http://example.com/123/$1,",
"127.0.0.1,^/api/(.*),@302 http://example.com/567/$1,",
},
}}, time.Millisecond*10)
go func() {
_ = svc.Run(context.Background())
}()
time.Sleep(50 * time.Millisecond)
h.Matcher = svc
h.Metrics = mgmt.NewMetrics()
go func() {
_ = h.Run(ctx)
}()
time.Sleep(10 * time.Millisecond)
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
{
req, err := http.NewRequest("GET", "http://localhost:"+strconv.Itoa(port)+"/api/something", nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusMovedPermanently, resp.StatusCode)
t.Logf("%+v", resp.Header)
assert.Equal(t, "http://example.com/123/something", resp.Header.Get("Location"))
}
{
req, err := http.NewRequest("GET", "http://127.0.0.1:"+strconv.Itoa(port)+"/api/something", nil)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusFound, resp.StatusCode)
t.Logf("%+v", resp.Header)
assert.Equal(t, "http://example.com/567/something", resp.Header.Get("Location"))
}
}
func TestHttp_DoLimitedReq(t *testing.T) {
port := rand.Intn(10000) + 40000
h := Http{Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond}, Address: fmt.Sprintf("127.0.0.1:%d", port),