1
0
mirror of https://github.com/kataras/iris.git synced 2026-01-23 11:56:00 +00:00

implement a rewrite middleware

This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-08-19 22:40:17 +03:00
parent dddfeb9ca9
commit 12737c5b7f
9 changed files with 410 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ Builtin Handlers
| Middleware | Example |
| -----------|-------------|
| [rewrite](rewrite) | [iris/_examples/routing/rewrite](https://github.com/kataras/iris/tree/master/_examples/routing/rewrite) |
| [basic authentication](basicauth) | [iris/_examples/auth/basicauth](https://github.com/kataras/iris/tree/master/_examples/auth/basicauth) |
| [request logger](logger) | [iris/_examples/logging/request-logger](https://github.com/kataras/iris/tree/master/_examples/logging/request-logger) |
| [HTTP method override](methodoverride) | [iris/middleware/methodoverride/methodoverride_test.go](https://github.com/kataras/iris/blob/master/middleware/methodoverride/methodoverride_test.go) |
@@ -29,7 +30,7 @@ Most of the experimental handlers are ported to work with _iris_'s handler form,
| [new relic](https://github.com/iris-contrib/middleware/tree/master/newrelic) | Official [New Relic Go Agent](https://github.com/newrelic/go-agent) | [iris-contrib/middleware/newrelic/_example](https://github.com/iris-contrib/middleware/tree/master/newrelic/_example) |
| [prometheus](https://github.com/iris-contrib/middleware/tree/master/prometheus)| Easily create metrics endpoint for the [prometheus](http://prometheus.io) instrumentation tool | [iris-contrib/middleware/prometheus/_example](https://github.com/iris-contrib/middleware/tree/master/prometheus/_example) |
| [casbin](https://github.com/iris-contrib/middleware/tree/master/casbin)| An authorization library that supports access control models like ACL, RBAC, ABAC | [iris-contrib/middleware/casbin/_examples](https://github.com/iris-contrib/middleware/tree/master/casbin/_examples) |
| [raven](https://github.com/iris-contrib/middleware/tree/master/raven)| Sentry client in Go | [iris-contrib/middleware/raven/_example](https://github.com/iris-contrib/middleware/blob/master/raven/_example/main.go) |
| [sentry-go (ex. raven)](https://github.com/getsentry/sentry-go/tree/master/iris)| Sentry client in Go | [sentry-go/example/iris](https://github.com/getsentry/sentry-go/blob/master/example/iris/main.go) | <!-- raven was deprecated by its company, the successor is sentry-go, they contain an Iris middleware. -->
| [csrf](https://github.com/iris-contrib/middleware/tree/master/csrf)| Cross-Site Request Forgery Protection | [iris-contrib/middleware/csrf/_example](https://github.com/iris-contrib/middleware/blob/master/csrf/_example/main.go) |
| [go-i18n](https://github.com/iris-contrib/middleware/tree/master/go-i18n)| i18n Iris Loader for nicksnyder/go-i18n | [iris-contrib/middleware/go-i18n/_example](https://github.com/iris-contrib/middleware/blob/master/go-i18n/_example/main.go) |
| [throttler](https://github.com/iris-contrib/middleware/tree/master/throttler)| Rate limiting access to HTTP endpoints | [iris-contrib/middleware/throttler/_example](https://github.com/iris-contrib/middleware/blob/master/throttler/_example/main.go) |

View File

@@ -0,0 +1,219 @@
package rewrite
import (
"encoding/json"
"fmt"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"github.com/kataras/iris/v12/context"
"gopkg.in/yaml.v3"
)
// Options holds the developer input to customize
// the redirects for the Rewrite Engine.
// Look the `New` package-level function.
type Options struct {
// RedirectMatch accepts a slice of lines
// of form:
// REDIRECT_CODE PATH_PATTERN TARGET_PATH
// Example: []{"301 /seo/(.*) /$1"}.
RedirectMatch []string `json:"redirectMatch" yaml:"RedirectMatch"`
}
// Engine is the rewrite engine master structure.
// Navigate through _examples/routing/rewrite for more.
type Engine struct {
redirects []*redirectMatch
}
// New returns a new Rewrite Engine based on "opts".
// It reports any parser error.
// See its `Handler` or `Wrapper` methods. Depending
// on the needs, select one.
func New(opts Options) (*Engine, error) {
redirects := make([]*redirectMatch, 0, len(opts.RedirectMatch))
for _, line := range opts.RedirectMatch {
r, err := parseRedirectMatchLine(line)
if err != nil {
return nil, err
}
redirects = append(redirects, r)
}
e := &Engine{
redirects: redirects,
}
return e, nil
}
// Handler returns a new rewrite Iris Handler.
// It panics on any error.
// Same as engine, _ := New(opts); engine.Handler.
// Usage:
// app.UseRouter(Handler(opts)).
func Handler(opts Options) context.Handler {
engine, err := New(opts)
if err != nil {
panic(err)
}
return engine.Handler
}
// Handler is an Iris Handler that can be used as a router or party or route middleware.
// For a global alternative, if you want to wrap the entire Iris Application
// use the `Wrapper` instead.
// Usage:
// app.UseRouter(engine.Handler)
func (e *Engine) Handler(ctx *context.Context) {
// We could also do that:
// but we don't.
// e.WrapRouter(ctx.ResponseWriter(), ctx.Request(), func(http.ResponseWriter, *http.Request) {
// ctx.Next()
// })
for _, rd := range e.redirects {
src := ctx.Path()
if !rd.isRelativePattern {
src = ctx.Request().URL.String()
}
if target, ok := rd.matchAndReplace(src); ok {
if target == src {
// this should never happen: StatusTooManyRequests.
// keep the router flow.
ctx.Next()
return
}
ctx.Redirect(target, rd.code)
return
}
}
ctx.Next()
}
// Wrapper wraps the entire Iris Router.
// Wrapper is a bit faster than Handler because it's executed
// even before any route matched and it stops on redirect pattern match.
// Use it to wrap the entire Iris Application, otherwise look `Handler` instead.
//
// Usage:
// app.WrapRouter(engine.Wrapper).
func (e *Engine) Wrapper(w http.ResponseWriter, r *http.Request, routeHandler http.HandlerFunc) {
for _, rd := range e.redirects {
src := r.URL.Path
if !rd.isRelativePattern {
src = r.URL.String()
}
if target, ok := rd.matchAndReplace(src); ok {
if target == src {
routeHandler(w, r)
return
}
http.Redirect(w, r, target, rd.code)
return
}
}
routeHandler(w, r)
}
type redirectMatch struct {
code int
pattern *regexp.Regexp
target string
isRelativePattern bool
}
func (r *redirectMatch) matchAndReplace(src string) (string, bool) {
if r.pattern.MatchString(src) {
if match := r.pattern.ReplaceAllString(src, r.target); match != "" {
return match, true
}
}
return "", false
}
func parseRedirectMatchLine(s string) (*redirectMatch, error) {
parts := strings.Split(strings.TrimSpace(s), " ")
if len(parts) != 3 {
return nil, fmt.Errorf("redirect match: invalid line: %s", s)
}
codeStr, pattern, target := parts[0], parts[1], parts[2]
for i, ch := range codeStr {
if !isDigit(ch) {
return nil, fmt.Errorf("redirect match: status code digits: %s [%d:%c]", codeStr, i, ch)
}
}
code, err := strconv.Atoi(codeStr)
if err != nil {
// this should not happen, we check abt digit
// and correctly position the error too but handle it.
return nil, fmt.Errorf("redirect match: status code digits: %s: %v", codeStr, err)
}
if code <= 0 {
code = http.StatusMovedPermanently
}
regex := regexp.MustCompile(pattern)
if regex.MatchString(target) {
return nil, fmt.Errorf("redirect match: loop detected: pattern: %s vs target: %s", pattern, target)
}
v := &redirectMatch{
code: code,
pattern: regex,
target: target,
isRelativePattern: pattern[0] == '/', // search by path.
}
return v, nil
}
func isDigit(ch rune) bool {
return '0' <= ch && ch <= '9'
}
// LoadOptions loads rewrite Options from a system file.
func LoadOptions(filename string) (opts Options) {
ext := ".yml"
if index := strings.LastIndexByte(filename, '.'); index > 1 && len(filename)-1 > index {
ext = filename[index:]
}
f, err := os.Open(filename)
if err != nil {
panic("iris: rewrite: " + err.Error())
}
defer f.Close()
switch ext {
case ".yaml", ".yml":
err = yaml.NewDecoder(f).Decode(&opts)
case ".json":
err = json.NewDecoder(f).Decode(&opts)
default:
panic("iris: rewrite: unexpected file extension: " + filename)
}
if err != nil {
panic("iris: rewrite: decode: " + err.Error())
}
return
}

View File

@@ -0,0 +1,89 @@
package rewrite
import "testing"
func TestRedirectMatch(t *testing.T) {
tests := []struct {
line string
parseErr string
inputs map[string]string // input, expected. Order should not matter.
}{
{
"301 /seo/(.*) /$1",
"",
map[string]string{
"/seo/path": "/path",
},
},
{
"301 /old(.*) /deprecated$1",
"",
map[string]string{
"/old": "/deprecated",
"/old/any": "/deprecated/any",
"/old/thing/here": "/deprecated/thing/here",
},
},
{
"301 /old(.*) /",
"",
map[string]string{
"/oldblabla": "/",
"/old/any": "/",
"/old/thing/here": "/",
},
},
{
"301 /old/(.*) /deprecated/$1",
"",
map[string]string{
"/old/": "/deprecated/",
"/old/any": "/deprecated/any",
"/old/thing/here": "/deprecated/thing/here",
},
},
{
"3d /seo/(.*) /$1",
"redirect match: status code digits: 3d [1:d]",
nil,
},
{
"301 /$1",
"redirect match: invalid line: 301 /$1",
nil,
},
{
"301 /* /$1",
"redirect match: loop detected: pattern: /* vs target: /$1",
nil,
},
{
"301 /* /",
"redirect match: loop detected: pattern: /* vs target: /",
nil,
},
}
for i, tt := range tests {
r, err := parseRedirectMatchLine(tt.line)
if err != nil {
if tt.parseErr == "" {
t.Fatalf("[%d] unexpected parse error: %v", i, err)
}
errStr := err.Error()
if tt.parseErr != err.Error() {
t.Fatalf("[%d] a parse error was expected but it differs: expected: %s but got: %s", i, tt.parseErr, errStr)
}
} else if tt.parseErr != "" {
t.Fatalf("[%d] expected an error of: %s but got nil", i, tt.parseErr)
}
for input, expected := range tt.inputs {
got, _ := r.matchAndReplace(input)
if expected != got {
t.Fatalf(`[%d:%s] expected: "%s" but got: "%s"`, i, input, expected, got)
}
}
}
}