feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
238
pkg/utils/directives/match.go
Normal file
238
pkg/utils/directives/match.go
Normal file
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package directives
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Following code copied from github.com/caddyserver/caddy/modules/caddyhttp/matchers.go
|
||||
|
||||
type (
|
||||
MatchPath []string
|
||||
)
|
||||
|
||||
func (m MatchPath) Match(req *http.Request) bool {
|
||||
// Even though RFC 9110 says that path matching is case-sensitive
|
||||
// (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3),
|
||||
// we do case-insensitive matching to mitigate security issues
|
||||
// related to differences between operating systems, applications,
|
||||
// etc; if case-sensitive matching is needed, the regex matcher
|
||||
// can be used instead.
|
||||
reqPath := strings.ToLower(req.URL.Path)
|
||||
|
||||
// See #2917; Windows ignores trailing dots and spaces
|
||||
// when accessing files (sigh), potentially causing a
|
||||
// security risk (cry) if PHP files end up being served
|
||||
// as static files, exposing the source code, instead of
|
||||
// being matched by *.php to be treated as PHP scripts.
|
||||
if runtime.GOOS == "windows" { // issue #5613
|
||||
reqPath = strings.TrimRight(reqPath, ". ")
|
||||
}
|
||||
|
||||
repl := req.Context().Value(ReplacerCtxKey).(*Replacer)
|
||||
|
||||
for _, matchPattern := range m {
|
||||
matchPattern = repl.ReplaceAll(matchPattern, "")
|
||||
|
||||
// special case: whole path is wildcard; this is unnecessary
|
||||
// as it matches all requests, which is the same as no matcher
|
||||
if matchPattern == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Clean the path, merge doubled slashes, etc.
|
||||
// This ensures maliciously crafted requests can't bypass
|
||||
// the path matcher. See #4407. Good security posture
|
||||
// requires that we should do all we can to reduce any
|
||||
// funny-looking paths into "normalized" forms such that
|
||||
// weird variants can't sneak by.
|
||||
//
|
||||
// How we clean the path depends on the kind of pattern:
|
||||
// we either merge slashes or we don't. If the pattern
|
||||
// has double slashes, we preserve them in the path.
|
||||
//
|
||||
// TODO: Despite the fact that the *vast* majority of path
|
||||
// matchers have only 1 pattern, a possible optimization is
|
||||
// to remember the cleaned form of the path for future
|
||||
// iterations; it's just that the way we clean depends on
|
||||
// the kind of pattern.
|
||||
|
||||
mergeSlashes := !strings.Contains(matchPattern, "//")
|
||||
|
||||
// if '%' appears in the match pattern, we interpret that to mean
|
||||
// the intent is to compare that part of the path in raw/escaped
|
||||
// space; i.e. "%40"=="%40", not "@", and "%2F"=="%2F", not "/"
|
||||
if strings.Contains(matchPattern, "%") {
|
||||
reqPathForPattern := CleanPath(req.URL.EscapedPath(), mergeSlashes)
|
||||
if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
|
||||
return true
|
||||
}
|
||||
|
||||
// doing prefix/suffix/substring matches doesn't make sense
|
||||
continue
|
||||
}
|
||||
|
||||
reqPathForPattern := CleanPath(reqPath, mergeSlashes)
|
||||
|
||||
// for substring, prefix, and suffix matching, only perform those
|
||||
// special, fast matches if they are the only wildcards in the pattern;
|
||||
// otherwise we assume a globular match if any * appears in the middle
|
||||
|
||||
// special case: first and last characters are wildcard,
|
||||
// treat it as a fast substring match
|
||||
if strings.Count(matchPattern, "*") == 2 &&
|
||||
strings.HasPrefix(matchPattern, "*") &&
|
||||
strings.HasSuffix(matchPattern, "*") &&
|
||||
strings.Count(matchPattern, "*") == 2 {
|
||||
if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// only perform prefix/suffix match if it is the only wildcard...
|
||||
// I think that is more correct most of the time
|
||||
if strings.Count(matchPattern, "*") == 1 {
|
||||
// special case: first character is a wildcard,
|
||||
// treat it as a fast suffix match
|
||||
if strings.HasPrefix(matchPattern, "*") {
|
||||
if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// special case: last character is a wildcard,
|
||||
// treat it as a fast prefix match
|
||||
if strings.HasSuffix(matchPattern, "*") {
|
||||
if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// at last, use globular matching, which also is exact matching
|
||||
// if there are no glob/wildcard chars; we ignore the error here
|
||||
// because we can't handle it anyway
|
||||
matches, _ := path.Match(matchPattern, reqPathForPattern)
|
||||
if matches {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
|
||||
// We would just compare the pattern against r.URL.Path,
|
||||
// but the pattern contains %, indicating that we should
|
||||
// compare at least some part of the path in raw/escaped
|
||||
// space, not normalized space; so we build the string we
|
||||
// will compare against by adding the normalized parts
|
||||
// of the path, then switching to the escaped parts where
|
||||
// the pattern hints to us wherever % is present.
|
||||
var sb strings.Builder
|
||||
|
||||
// iterate the pattern and escaped path in lock-step;
|
||||
// increment iPattern every time we consume a char from the pattern,
|
||||
// increment iPath every time we consume a char from the path;
|
||||
// iPattern and iPath are our cursors/iterator positions for each string
|
||||
var iPattern, iPath int
|
||||
for {
|
||||
if iPattern >= len(matchPath) || iPath >= len(escapedPath) {
|
||||
break
|
||||
}
|
||||
|
||||
// get the next character from the request path
|
||||
|
||||
pathCh := string(escapedPath[iPath])
|
||||
var escapedPathCh string
|
||||
|
||||
// normalize (decode) escape sequences
|
||||
if pathCh == "%" && len(escapedPath) >= iPath+3 {
|
||||
// hold onto this in case we find out the intent is to match in escaped space here;
|
||||
// we lowercase it even though technically the spec says: "For consistency, URI
|
||||
// producers and normalizers should use uppercase hexadecimal digits for all percent-
|
||||
// encodings" (RFC 3986 section 2.1) - we lowercased the matcher pattern earlier in
|
||||
// provisioning so we do the same here to gain case-insensitivity in equivalence;
|
||||
// besides, this string is never shown visibly
|
||||
escapedPathCh = strings.ToLower(escapedPath[iPath : iPath+3])
|
||||
|
||||
var err error
|
||||
pathCh, err = url.PathUnescape(escapedPathCh)
|
||||
if err != nil {
|
||||
// should be impossible unless EscapedPath() is giving us an invalid sequence!
|
||||
return false
|
||||
}
|
||||
iPath += 2 // escape sequence is 2 bytes longer than normal char
|
||||
}
|
||||
|
||||
// now get the next character from the pattern
|
||||
|
||||
normalize := true
|
||||
switch matchPath[iPattern] {
|
||||
case '%':
|
||||
// escape sequence
|
||||
|
||||
// if not a wildcard ("%*"), compare literally; consume next two bytes of pattern
|
||||
if len(matchPath) >= iPattern+3 && matchPath[iPattern+1] != '*' {
|
||||
sb.WriteString(escapedPathCh)
|
||||
iPath++
|
||||
iPattern += 2
|
||||
break
|
||||
}
|
||||
|
||||
// escaped wildcard sequence; consume next byte only ('*')
|
||||
iPattern++
|
||||
normalize = false
|
||||
|
||||
fallthrough
|
||||
case '*':
|
||||
// wildcard, so consume until next matching character
|
||||
remaining := escapedPath[iPath:]
|
||||
until := len(escapedPath) - iPath // go until end of string...
|
||||
if iPattern < len(matchPath)-1 { // ...unless the * is not at the end
|
||||
nextCh := matchPath[iPattern+1]
|
||||
until = strings.IndexByte(remaining, nextCh)
|
||||
if until == -1 {
|
||||
// terminating char of wildcard span not found, so definitely no match
|
||||
return false
|
||||
}
|
||||
}
|
||||
if until == 0 {
|
||||
// empty span; nothing to add on this iteration
|
||||
break
|
||||
}
|
||||
next := remaining[:until]
|
||||
if normalize {
|
||||
var err error
|
||||
next, err = url.PathUnescape(next)
|
||||
if err != nil {
|
||||
return false // should be impossible anyway
|
||||
}
|
||||
}
|
||||
sb.WriteString(next)
|
||||
iPath += until
|
||||
default:
|
||||
sb.WriteString(pathCh)
|
||||
iPath++
|
||||
}
|
||||
|
||||
iPattern++
|
||||
}
|
||||
|
||||
// we can now treat rawpath globs (%*) as regular globs (*)
|
||||
matchPath = strings.ReplaceAll(matchPath, "%*", "*")
|
||||
|
||||
// ignore error here because we can't handle it anyway=
|
||||
matches, _ := path.Match(matchPath, sb.String())
|
||||
return matches
|
||||
}
|
||||
269
pkg/utils/directives/match_test.go
Normal file
269
pkg/utils/directives/match_test.go
Normal file
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package directives
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathMatcher(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
match MatchPath // not URI-encoded because not parsing from a URI
|
||||
input string // should be valid URI encoding (escaped) since it will become part of a request
|
||||
expect bool
|
||||
provisionErr bool
|
||||
}{
|
||||
{
|
||||
match: MatchPath{},
|
||||
input: "/",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/"},
|
||||
input: "/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar"},
|
||||
input: "/",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar"},
|
||||
input: "/foo/bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar/"},
|
||||
input: "/foo/bar",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar/"},
|
||||
input: "/foo/bar/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar/", "/other"},
|
||||
input: "/other/",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar/", "/other"},
|
||||
input: "/other",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"*.ext"},
|
||||
input: "/foo/bar.ext",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"*.php"},
|
||||
input: "/index.PHP",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"*.ext"},
|
||||
input: "/foo/bar.ext",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/*/baz"},
|
||||
input: "/foo/bar/baz",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/*/baz/bam"},
|
||||
input: "/foo/bar/bam",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"*substring*"},
|
||||
input: "/foo/substring/bar.txt",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo"},
|
||||
input: "/foo/bar",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo"},
|
||||
input: "/foo/bar",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo"},
|
||||
input: "/FOO",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo*"},
|
||||
input: "/FOOOO",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar.txt"},
|
||||
input: "/foo/BAR.txt",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo*"},
|
||||
input: "//foo/bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo"},
|
||||
input: "//foo",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"//foo"},
|
||||
input: "/foo",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"//foo"},
|
||||
input: "//foo",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo//*"},
|
||||
input: "/foo//bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo//*"},
|
||||
input: "/foo/%2Fbar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/%2F*"},
|
||||
input: "/foo//bar",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo//bar"},
|
||||
input: "/foo//bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/*//bar"},
|
||||
input: "/foo///bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/%*//bar"},
|
||||
input: "/foo///bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/%*//bar"},
|
||||
input: "/foo//%2Fbar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo*"},
|
||||
input: "/%2F/foo",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"*"},
|
||||
input: "/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"*"},
|
||||
input: "/foo/bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"**"},
|
||||
input: "/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"**"},
|
||||
input: "/foo/bar",
|
||||
expect: true,
|
||||
},
|
||||
// notice these next three test cases are the same normalized path but are written differently
|
||||
{
|
||||
match: MatchPath{"/%25@.txt"},
|
||||
input: "/%25@.txt",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/%25@.txt"},
|
||||
input: "/%25%40.txt",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/%25%40.txt"},
|
||||
input: "/%25%40.txt",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/bands/*/*"},
|
||||
input: "/bands/AC%2FDC/T.N.T",
|
||||
expect: false, // because * operates in normalized space
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/bands/%*/%*"},
|
||||
input: "/bands/AC%2FDC/T.N.T",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/bands/%*/%*"},
|
||||
input: "/bands/AC/DC/T.N.T",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/bands/%*"},
|
||||
input: "/bands/AC/DC",
|
||||
expect: false, // not a suffix match
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/bands/%*"},
|
||||
input: "/bands/AC%2FDC",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo%2fbar/baz"},
|
||||
input: "/foo%2Fbar/baz",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo%2fbar/baz"},
|
||||
input: "/foo/bar/baz",
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
match: MatchPath{"/foo/bar/baz"},
|
||||
input: "/foo%2fbar/baz",
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
u, err := url.ParseRequestURI(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d (%v): Invalid request URI (should be rejected by Go's HTTP server): %v", i, tc.input, err)
|
||||
}
|
||||
req := &http.Request{URL: u}
|
||||
repl := NewReplacer()
|
||||
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
actual := tc.match.Match(req)
|
||||
if actual != tc.expect {
|
||||
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
338
pkg/utils/directives/replace.go
Normal file
338
pkg/utils/directives/replace.go
Normal file
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package directives
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Following code copied from github.com/caddyserver/caddy/modules/caddyhttp/rewrite/rewrite.go
|
||||
|
||||
type substrReplacer struct {
|
||||
Find string `json:"find,omitempty"`
|
||||
Replace string `json:"replace,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
}
|
||||
|
||||
func (rep substrReplacer) do(r *http.Request, repl *Replacer) {
|
||||
if rep.Find == "" {
|
||||
return
|
||||
}
|
||||
|
||||
lim := rep.Limit
|
||||
if lim == 0 {
|
||||
lim = -1
|
||||
}
|
||||
|
||||
find := repl.ReplaceAll(rep.Find, "")
|
||||
replace := repl.ReplaceAll(rep.Replace, "")
|
||||
|
||||
mergeSlashes := !strings.Contains(rep.Find, "//")
|
||||
|
||||
changePath(r, func(pathOrRawPath string) string {
|
||||
return strings.Replace(CleanPath(pathOrRawPath, mergeSlashes), find, replace, lim)
|
||||
})
|
||||
|
||||
r.URL.RawQuery = strings.Replace(r.URL.RawQuery, find, replace, lim)
|
||||
}
|
||||
|
||||
type regexReplacer struct {
|
||||
Find string `json:"find,omitempty"`
|
||||
Replace string `json:"replace,omitempty"`
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
func (rep regexReplacer) do(r *http.Request, repl *Replacer) {
|
||||
if rep.Find == "" || rep.re == nil {
|
||||
return
|
||||
}
|
||||
replace := repl.ReplaceAll(rep.Replace, "")
|
||||
changePath(r, func(pathOrRawPath string) string {
|
||||
return rep.re.ReplaceAllString(pathOrRawPath, replace)
|
||||
})
|
||||
}
|
||||
|
||||
func NewReplacer() *Replacer {
|
||||
rep := &Replacer{
|
||||
static: make(map[string]any),
|
||||
}
|
||||
rep.providers = []ReplacerFunc{
|
||||
globalDefaultReplacements,
|
||||
rep.fromStatic,
|
||||
}
|
||||
return rep
|
||||
}
|
||||
|
||||
type Replacer struct {
|
||||
providers []ReplacerFunc
|
||||
static map[string]any
|
||||
}
|
||||
|
||||
func (r *Replacer) Map(mapFunc ReplacerFunc) {
|
||||
r.providers = append(r.providers, mapFunc)
|
||||
}
|
||||
|
||||
func (r *Replacer) Set(variable string, value any) {
|
||||
r.static[variable] = value
|
||||
}
|
||||
|
||||
func (r *Replacer) Get(variable string) (any, bool) {
|
||||
for _, mapFunc := range r.providers {
|
||||
if val, ok := mapFunc(variable); ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Replacer) GetString(variable string) (string, bool) {
|
||||
s, found := r.Get(variable)
|
||||
return ToString(s), found
|
||||
}
|
||||
|
||||
func (r *Replacer) Delete(variable string) {
|
||||
delete(r.static, variable)
|
||||
}
|
||||
|
||||
func (r *Replacer) fromStatic(key string) (any, bool) {
|
||||
val, ok := r.static[key]
|
||||
return val, ok
|
||||
}
|
||||
|
||||
func (r *Replacer) ReplaceOrErr(input string, errOnEmpty, errOnUnknown bool) (string, error) {
|
||||
return r.replace(input, "", false, errOnEmpty, errOnUnknown, nil)
|
||||
}
|
||||
|
||||
func (r *Replacer) ReplaceKnown(input, empty string) string {
|
||||
out, _ := r.replace(input, empty, false, false, false, nil)
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Replacer) ReplaceAll(input, empty string) string {
|
||||
out, _ := r.replace(input, empty, true, false, false, nil)
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *Replacer) ReplaceFunc(input string, f ReplacementFunc) (string, error) {
|
||||
return r.replace(input, "", true, false, false, f)
|
||||
}
|
||||
|
||||
func (r *Replacer) replace(input, empty string,
|
||||
treatUnknownAsEmpty, errOnEmpty, errOnUnknown bool,
|
||||
f ReplacementFunc,
|
||||
) (string, error) {
|
||||
if !strings.Contains(input, string(phOpen)) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
// it is reasonable to assume that the output
|
||||
// will be approximately as long as the input
|
||||
sb.Grow(len(input))
|
||||
|
||||
// iterate the input to find each placeholder
|
||||
var lastWriteCursor int
|
||||
|
||||
// fail fast if too many placeholders are unclosed
|
||||
var unclosedCount int
|
||||
|
||||
scan:
|
||||
for i := 0; i < len(input); i++ {
|
||||
// check for escaped braces
|
||||
if i > 0 && input[i-1] == phEscape && (input[i] == phClose || input[i] == phOpen) {
|
||||
sb.WriteString(input[lastWriteCursor : i-1])
|
||||
lastWriteCursor = i
|
||||
continue
|
||||
}
|
||||
|
||||
if input[i] != phOpen {
|
||||
continue
|
||||
}
|
||||
|
||||
// our iterator is now on an unescaped open brace (start of placeholder)
|
||||
|
||||
// too many unclosed placeholders in absolutely ridiculous input can be extremely slow (issue #4170)
|
||||
if unclosedCount > 100 {
|
||||
return "", fmt.Errorf("too many unclosed placeholders")
|
||||
}
|
||||
|
||||
// find the end of the placeholder
|
||||
end := strings.Index(input[i:], string(phClose)) + i
|
||||
if end < i {
|
||||
unclosedCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// if necessary look for the first closing brace that is not escaped
|
||||
for end > 0 && end < len(input)-1 && input[end-1] == phEscape {
|
||||
nextEnd := strings.Index(input[end+1:], string(phClose))
|
||||
if nextEnd < 0 {
|
||||
unclosedCount++
|
||||
continue scan
|
||||
}
|
||||
end += nextEnd + 1
|
||||
}
|
||||
|
||||
// write the substring from the last cursor to this point
|
||||
sb.WriteString(input[lastWriteCursor:i])
|
||||
|
||||
// trim opening bracket
|
||||
key := input[i+1 : end]
|
||||
|
||||
// try to get a value for this key, handle empty values accordingly
|
||||
val, found := r.Get(key)
|
||||
if !found {
|
||||
// placeholder is unknown (unrecognized); handle accordingly
|
||||
if errOnUnknown {
|
||||
return "", fmt.Errorf("unrecognized placeholder %s%s%s",
|
||||
string(phOpen), key, string(phClose))
|
||||
} else if !treatUnknownAsEmpty {
|
||||
// if treatUnknownAsEmpty is true, we'll handle an empty
|
||||
// val later; so only continue otherwise
|
||||
lastWriteCursor = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// apply any transformations
|
||||
if f != nil {
|
||||
var err error
|
||||
val, err = f(key, val)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
valStr := ToString(val)
|
||||
|
||||
if valStr == "" {
|
||||
if errOnEmpty {
|
||||
return "", fmt.Errorf("evaluated placeholder %s%s%s is empty",
|
||||
string(phOpen), key, string(phClose))
|
||||
} else if empty != "" {
|
||||
sb.WriteString(empty)
|
||||
}
|
||||
} else {
|
||||
sb.WriteString(valStr)
|
||||
}
|
||||
|
||||
i = end
|
||||
lastWriteCursor = i + 1
|
||||
}
|
||||
|
||||
sb.WriteString(input[lastWriteCursor:])
|
||||
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
func ToString(val any) string {
|
||||
switch v := val.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return v
|
||||
case fmt.Stringer:
|
||||
return v.String()
|
||||
case error:
|
||||
return v.Error()
|
||||
case byte:
|
||||
return string(v)
|
||||
case []byte:
|
||||
return string(v)
|
||||
case []rune:
|
||||
return string(v)
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int32:
|
||||
return strconv.Itoa(int(v))
|
||||
case int64:
|
||||
return strconv.Itoa(int(v))
|
||||
case uint:
|
||||
return strconv.Itoa(int(v))
|
||||
case uint32:
|
||||
return strconv.Itoa(int(v))
|
||||
case uint64:
|
||||
return strconv.Itoa(int(v))
|
||||
case float32:
|
||||
return strconv.FormatFloat(float64(v), 'f', -1, 32)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case bool:
|
||||
if v {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
default:
|
||||
return fmt.Sprintf("%+v", v)
|
||||
}
|
||||
}
|
||||
|
||||
type ReplacerFunc func(key string) (any, bool)
|
||||
|
||||
func globalDefaultReplacements(key string) (any, bool) {
|
||||
const envPrefix = "env."
|
||||
if strings.HasPrefix(key, envPrefix) {
|
||||
return os.Getenv(key[len(envPrefix):]), true
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "system.hostname":
|
||||
name, _ := os.Hostname()
|
||||
return name, true
|
||||
case "system.slash":
|
||||
return string(filepath.Separator), true
|
||||
case "system.os":
|
||||
return runtime.GOOS, true
|
||||
case "system.wd":
|
||||
// OK if there is an error; just return empty string
|
||||
wd, _ := os.Getwd()
|
||||
return wd, true
|
||||
case "system.arch":
|
||||
return runtime.GOARCH, true
|
||||
case "time.now":
|
||||
return nowFunc(), true
|
||||
case "time.now.http":
|
||||
return nowFunc().UTC().Format(http.TimeFormat), true
|
||||
case "time.now.common_log":
|
||||
return nowFunc().Format("02/Jan/2006:15:04:05 -0700"), true
|
||||
case "time.now.year":
|
||||
return strconv.Itoa(nowFunc().Year()), true
|
||||
case "time.now.unix":
|
||||
return strconv.FormatInt(nowFunc().Unix(), 10), true
|
||||
case "time.now.unix_ms":
|
||||
return strconv.FormatInt(nowFunc().UnixNano()/int64(time.Millisecond), 10), true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ReplacementFunc is a function that is called when a
|
||||
// replacement is being performed. It receives the
|
||||
// variable (i.e. placeholder name) and the value that
|
||||
// will be the replacement, and returns the value that
|
||||
// will actually be the replacement, or an error. Note
|
||||
// that errors are sometimes ignored by replacers.
|
||||
type ReplacementFunc func(variable string, val any) (any, error)
|
||||
|
||||
// nowFunc is a variable so tests can change it
|
||||
// in order to obtain a deterministic time.
|
||||
var nowFunc = time.Now
|
||||
|
||||
type ContextKey string
|
||||
|
||||
// ReplacerCtxKey is the context key for a replacer.
|
||||
const ReplacerCtxKey ContextKey = "replacer"
|
||||
|
||||
const phOpen, phClose, phEscape = '{', '}', '\\'
|
||||
472
pkg/utils/directives/rewrite.go
Normal file
472
pkg/utils/directives/rewrite.go
Normal file
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package directives
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Following code copied from github.com/caddyserver/caddy/modules/caddyhttp/rewrite/rewrite.go
|
||||
|
||||
type Rewrite struct {
|
||||
Method string `json:"method,omitempty"`
|
||||
URI string `json:"uri,omitempty"`
|
||||
StripPathPrefix string `json:"strip_path_prefix,omitempty"`
|
||||
StripPathSuffix string `json:"strip_path_suffix,omitempty"`
|
||||
URISubstring []substrReplacer `json:"uri_substring,omitempty"`
|
||||
PathRegexp []*regexReplacer `json:"path_regexp,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Rewrite) Rewrite(req *http.Request, repl *Replacer) bool {
|
||||
oldMethod := req.Method
|
||||
oldURI := req.RequestURI
|
||||
|
||||
// method
|
||||
if r.Method != "" {
|
||||
req.Method = strings.ToUpper(repl.ReplaceAll(r.Method, ""))
|
||||
}
|
||||
|
||||
// uri (path, query string and... fragment, because why not)
|
||||
if uri := r.URI; uri != "" {
|
||||
// find the bounds of each part of the URI that exist
|
||||
pathStart, qsStart, fragStart := -1, -1, -1
|
||||
pathEnd, qsEnd := -1, -1
|
||||
loop:
|
||||
for i, ch := range uri {
|
||||
switch {
|
||||
case ch == '?' && qsStart < 0:
|
||||
pathEnd, qsStart = i, i+1
|
||||
case ch == '#' && fragStart < 0: // everything after fragment is fragment (very clear in RFC 3986 section 4.2)
|
||||
if qsStart < 0 {
|
||||
pathEnd = i
|
||||
} else {
|
||||
qsEnd = i
|
||||
}
|
||||
fragStart = i + 1
|
||||
break loop
|
||||
case pathStart < 0 && qsStart < 0:
|
||||
pathStart = i
|
||||
}
|
||||
}
|
||||
if pathStart >= 0 && pathEnd < 0 {
|
||||
pathEnd = len(uri)
|
||||
}
|
||||
if qsStart >= 0 && qsEnd < 0 {
|
||||
qsEnd = len(uri)
|
||||
}
|
||||
|
||||
// isolate the three main components of the URI
|
||||
var path, query, frag string
|
||||
if pathStart > -1 {
|
||||
path = uri[pathStart:pathEnd]
|
||||
}
|
||||
if qsStart > -1 {
|
||||
query = uri[qsStart:qsEnd]
|
||||
}
|
||||
if fragStart > -1 {
|
||||
frag = uri[fragStart:]
|
||||
}
|
||||
|
||||
// build components which are specified, and store them
|
||||
// in a temporary variable so that they all read the
|
||||
// same version of the URI
|
||||
var newPath, newQuery, newFrag string
|
||||
|
||||
if path != "" {
|
||||
// replace the `path` placeholder to escaped path
|
||||
pathPlaceholder := "{http.request.uri.path}"
|
||||
if strings.Contains(path, pathPlaceholder) {
|
||||
path = strings.ReplaceAll(path, pathPlaceholder, req.URL.EscapedPath())
|
||||
}
|
||||
|
||||
newPath = repl.ReplaceAll(path, "")
|
||||
}
|
||||
|
||||
// before continuing, we need to check if a query string
|
||||
// snuck into the path component during replacements
|
||||
if before, after, found := strings.Cut(newPath, "?"); found {
|
||||
// recompute; new path contains a query string
|
||||
var injectedQuery string
|
||||
newPath, injectedQuery = before, after
|
||||
// don't overwrite explicitly-configured query string
|
||||
if query == "" {
|
||||
query = injectedQuery
|
||||
}
|
||||
}
|
||||
|
||||
if query != "" {
|
||||
newQuery = buildQueryString(query, repl)
|
||||
}
|
||||
if frag != "" {
|
||||
newFrag = repl.ReplaceAll(frag, "")
|
||||
}
|
||||
|
||||
// update the URI with the new components
|
||||
// only after building them
|
||||
if pathStart >= 0 {
|
||||
if path, err := url.PathUnescape(newPath); err != nil {
|
||||
req.URL.Path = newPath
|
||||
} else {
|
||||
req.URL.Path = path
|
||||
}
|
||||
}
|
||||
if qsStart >= 0 {
|
||||
req.URL.RawQuery = newQuery
|
||||
}
|
||||
if fragStart >= 0 {
|
||||
req.URL.Fragment = newFrag
|
||||
}
|
||||
}
|
||||
|
||||
// strip path prefix or suffix
|
||||
if r.StripPathPrefix != "" {
|
||||
prefix := repl.ReplaceAll(r.StripPathPrefix, "")
|
||||
mergeSlashes := !strings.Contains(prefix, "//")
|
||||
changePath(req, func(escapedPath string) string {
|
||||
escapedPath = CleanPath(escapedPath, mergeSlashes)
|
||||
return trimPathPrefix(escapedPath, prefix)
|
||||
})
|
||||
}
|
||||
if r.StripPathSuffix != "" {
|
||||
suffix := repl.ReplaceAll(r.StripPathSuffix, "")
|
||||
mergeSlashes := !strings.Contains(suffix, "//")
|
||||
changePath(req, func(escapedPath string) string {
|
||||
escapedPath = CleanPath(escapedPath, mergeSlashes)
|
||||
return reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix)))
|
||||
})
|
||||
}
|
||||
|
||||
// substring replacements in URI
|
||||
for _, rep := range r.URISubstring {
|
||||
rep.do(req, repl)
|
||||
}
|
||||
|
||||
// regular expression replacements on the path
|
||||
for _, rep := range r.PathRegexp {
|
||||
rep.do(req, repl)
|
||||
}
|
||||
|
||||
// update the encoded copy of the URI
|
||||
req.RequestURI = req.URL.RequestURI()
|
||||
|
||||
// return true if anything changed
|
||||
return req.Method != oldMethod || req.RequestURI != oldURI
|
||||
}
|
||||
|
||||
func buildQueryString(qs string, repl *Replacer) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// first component must be key, which is the same
|
||||
// as if we just wrote a value in previous iteration
|
||||
wroteVal := true
|
||||
|
||||
for len(qs) > 0 {
|
||||
// determine the end of this component, which will be at
|
||||
// the next equal sign or ampersand, whichever comes first
|
||||
nextEq, nextAmp := strings.Index(qs, "="), strings.Index(qs, "&")
|
||||
ampIsNext := nextAmp >= 0 && (nextAmp < nextEq || nextEq < 0)
|
||||
end := len(qs) // assume no delimiter remains...
|
||||
if ampIsNext {
|
||||
end = nextAmp // ...unless ampersand is first...
|
||||
} else if nextEq >= 0 && (nextEq < nextAmp || nextAmp < 0) {
|
||||
end = nextEq // ...or unless equal is first.
|
||||
}
|
||||
|
||||
// consume the component and write the result
|
||||
comp := qs[:end]
|
||||
comp, _ = repl.ReplaceFunc(comp, func(name string, val any) (any, error) {
|
||||
if name == "http.request.uri.query" && wroteVal {
|
||||
return val, nil // already escaped
|
||||
}
|
||||
var valStr string
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
valStr = v
|
||||
case fmt.Stringer:
|
||||
valStr = v.String()
|
||||
case int:
|
||||
valStr = strconv.Itoa(v)
|
||||
default:
|
||||
valStr = fmt.Sprintf("%+v", v)
|
||||
}
|
||||
return url.QueryEscape(valStr), nil
|
||||
})
|
||||
if end < len(qs) {
|
||||
end++ // consume delimiter
|
||||
}
|
||||
qs = qs[end:]
|
||||
|
||||
// if previous iteration wrote a value,
|
||||
// that means we are writing a key
|
||||
if wroteVal {
|
||||
if sb.Len() > 0 && len(comp) > 0 {
|
||||
sb.WriteRune('&')
|
||||
}
|
||||
} else {
|
||||
sb.WriteRune('=')
|
||||
}
|
||||
sb.WriteString(comp)
|
||||
|
||||
// remember for the next iteration that we just wrote a value,
|
||||
// which means the next iteration MUST write a key
|
||||
wroteVal = ampIsNext
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func trimPathPrefix(escapedPath, prefix string) string {
|
||||
var iPath, iPrefix int
|
||||
for {
|
||||
if iPath >= len(escapedPath) || iPrefix >= len(prefix) {
|
||||
break
|
||||
}
|
||||
|
||||
prefixCh := prefix[iPrefix]
|
||||
ch := string(escapedPath[iPath])
|
||||
|
||||
if ch == "%" && prefixCh != '%' && len(escapedPath) >= iPath+3 {
|
||||
var err error
|
||||
ch, err = url.PathUnescape(escapedPath[iPath : iPath+3])
|
||||
if err != nil {
|
||||
// should be impossible unless EscapedPath() is returning invalid values!
|
||||
return escapedPath
|
||||
}
|
||||
iPath += 2
|
||||
}
|
||||
|
||||
// prefix comparisons are case-insensitive to consistency with
|
||||
// path matcher, which is case-insensitive for good reasons
|
||||
if !strings.EqualFold(ch, string(prefixCh)) {
|
||||
return escapedPath
|
||||
}
|
||||
|
||||
iPath++
|
||||
iPrefix++
|
||||
}
|
||||
|
||||
// if we iterated through the entire prefix, we found it, so trim it
|
||||
if iPath >= len(prefix) {
|
||||
return escapedPath[iPath:]
|
||||
}
|
||||
|
||||
// otherwise we did not find the prefix
|
||||
return escapedPath
|
||||
}
|
||||
|
||||
func reverse(s string) string {
|
||||
r := []rune(s)
|
||||
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
return string(r)
|
||||
}
|
||||
|
||||
func changePath(req *http.Request, newVal func(pathOrRawPath string) string) {
|
||||
req.URL.RawPath = newVal(req.URL.EscapedPath())
|
||||
if p, err := url.PathUnescape(req.URL.RawPath); err == nil && p != "" {
|
||||
req.URL.Path = p
|
||||
} else {
|
||||
req.URL.Path = newVal(req.URL.Path)
|
||||
}
|
||||
// RawPath is only set if it's different from the normalized Path (std lib)
|
||||
if req.URL.RawPath == req.URL.Path {
|
||||
req.URL.RawPath = ""
|
||||
}
|
||||
}
|
||||
|
||||
func CleanPath(p string, collapseSlashes bool) string {
|
||||
if collapseSlashes {
|
||||
return cleanPath(p)
|
||||
}
|
||||
|
||||
// insert an invalid/impossible URI character into each two consecutive
|
||||
// slashes to expand empty path segments; then clean the path as usual,
|
||||
// and then remove the remaining temporary characters.
|
||||
const tmpCh = 0xff
|
||||
var sb strings.Builder
|
||||
for i, ch := range p {
|
||||
if ch == '/' && i > 0 && p[i-1] == '/' {
|
||||
sb.WriteByte(tmpCh)
|
||||
}
|
||||
sb.WriteRune(ch)
|
||||
}
|
||||
halfCleaned := cleanPath(sb.String())
|
||||
halfCleaned = strings.ReplaceAll(halfCleaned, string([]byte{tmpCh}), "")
|
||||
|
||||
return halfCleaned
|
||||
}
|
||||
|
||||
// cleanPath does path.Clean(p) but preserves any trailing slash.
|
||||
func cleanPath(p string) string {
|
||||
cleaned := path.Clean(p)
|
||||
if cleaned != "/" && strings.HasSuffix(p, "/") {
|
||||
cleaned = cleaned + "/"
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
type RewriteRule struct {
|
||||
Match MatchPath
|
||||
Rewrite Rewrite
|
||||
}
|
||||
|
||||
func (rr *RewriteRule) Exec(req *http.Request) (change bool, err error) {
|
||||
var repl *Replacer
|
||||
|
||||
defer func() {
|
||||
if panicErr := recover(); panicErr != nil {
|
||||
err = fmt.Errorf("RewriteRule Err %v", panicErr)
|
||||
}
|
||||
}()
|
||||
replCtx := req.Context().Value(ReplacerCtxKey)
|
||||
if replCtx == nil || replCtx.(*Replacer) == nil {
|
||||
repl := NewReplacer()
|
||||
repl.Set("query", req.URL.RawQuery)
|
||||
repl.Set("path", req.URL.Path)
|
||||
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
} else {
|
||||
repl = replCtx.(*Replacer)
|
||||
}
|
||||
if rr.Match == nil || rr.Match.Match(req) {
|
||||
return rr.Rewrite.Rewrite(req, repl), nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type DirectiveFilter func(rr *[]RewriteRule, expr []string, exprLen int)
|
||||
type WithDirectiveFilter func(wf *[]DirectiveFilter)
|
||||
|
||||
func NewRewriteRulesWithOptions(rules []string, directiveFilters ...WithDirectiveFilter) []RewriteRule {
|
||||
var rewriteRules = make([]RewriteRule, 0, 1)
|
||||
if rules == nil {
|
||||
return rewriteRules
|
||||
}
|
||||
var filter = make([]DirectiveFilter, 0, 1)
|
||||
// inject directiveFilter for filter rewrite/replace/path_regexp
|
||||
for _, directiveFilter := range directiveFilters {
|
||||
directiveFilter(&filter)
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
expr := strings.Split(rule, " ")
|
||||
exprLen := len(expr)
|
||||
for _, directiveFilter := range filter {
|
||||
directiveFilter(&rewriteRules, expr, exprLen)
|
||||
}
|
||||
}
|
||||
return rewriteRules
|
||||
}
|
||||
|
||||
func WithRewriteFilter(df *[]DirectiveFilter) {
|
||||
filterFn := func(rr *[]RewriteRule, expr []string, exprLen int) {
|
||||
if exprLen >= 2 {
|
||||
rewrite := Rewrite{
|
||||
URI: expr[1],
|
||||
}
|
||||
matchRule := []string{
|
||||
expr[0],
|
||||
}
|
||||
*rr = append(*rr, RewriteRule{
|
||||
matchRule,
|
||||
rewrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
*df = append(*df, filterFn)
|
||||
}
|
||||
|
||||
func WithReplaceFilter(df *[]DirectiveFilter) {
|
||||
filterFn := func(rr *[]RewriteRule, expr []string, exprLen int) {
|
||||
if exprLen >= 2 {
|
||||
rewrite := Rewrite{
|
||||
URISubstring: []substrReplacer{
|
||||
{
|
||||
Find: expr[0],
|
||||
Replace: expr[1],
|
||||
Limit: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
*rr = append(*rr, RewriteRule{
|
||||
nil,
|
||||
rewrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
*df = append(*df, filterFn)
|
||||
}
|
||||
|
||||
func WithPathRegexpFilter(df *[]DirectiveFilter) {
|
||||
filterFn := func(rr *[]RewriteRule, expr []string, exprLen int) {
|
||||
if exprLen >= 2 {
|
||||
re, err := regexp.Compile(expr[0])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rewrite := Rewrite{
|
||||
PathRegexp: []*regexReplacer{
|
||||
{
|
||||
Find: expr[0],
|
||||
Replace: expr[1],
|
||||
re: re,
|
||||
},
|
||||
},
|
||||
}
|
||||
*rr = append(*rr, RewriteRule{
|
||||
nil,
|
||||
rewrite,
|
||||
})
|
||||
}
|
||||
}
|
||||
*df = append(*df, filterFn)
|
||||
}
|
||||
|
||||
func WithStripPrefixFilter(df *[]DirectiveFilter) {
|
||||
filterFn := func(rr *[]RewriteRule, expr []string, exprLen int) {
|
||||
if exprLen >= 1 {
|
||||
*rr = append(*rr, RewriteRule{
|
||||
nil,
|
||||
Rewrite{
|
||||
StripPathPrefix: expr[0],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
*df = append(*df, filterFn)
|
||||
}
|
||||
|
||||
func WithStripSuffixFilter(df *[]DirectiveFilter) {
|
||||
filterFn := func(rr *[]RewriteRule, expr []string, exprLen int) {
|
||||
if exprLen >= 1 {
|
||||
*rr = append(*rr, RewriteRule{
|
||||
nil,
|
||||
Rewrite{
|
||||
StripPathSuffix: expr[0],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
*df = append(*df, filterFn)
|
||||
}
|
||||
|
||||
func HandlerRequest(req *http.Request, rules []string, directiveFilters ...WithDirectiveFilter) error {
|
||||
rewriteRules := NewRewriteRulesWithOptions(rules, directiveFilters...)
|
||||
for _, rewriteRule := range rewriteRules {
|
||||
if _, err := rewriteRule.Exec(req); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
225
pkg/utils/directives/rewrite_test.go
Normal file
225
pkg/utils/directives/rewrite_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package directives
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
repl := NewReplacer()
|
||||
|
||||
for i, tc := range []struct {
|
||||
input, expect *http.Request
|
||||
rule Rewrite
|
||||
}{
|
||||
{
|
||||
rule: Rewrite{StripPathPrefix: "/api"},
|
||||
input: newRequest(t, "GET", "/api"),
|
||||
expect: newRequest(t, "GET", "/"),
|
||||
},
|
||||
{
|
||||
rule: Rewrite{StripPathSuffix: ".html"},
|
||||
input: newRequest(t, "GET", "/index.html"),
|
||||
expect: newRequest(t, "GET", "/index"),
|
||||
},
|
||||
{
|
||||
rule: Rewrite{URISubstring: []substrReplacer{
|
||||
{
|
||||
Find: "/docs/",
|
||||
Replace: "/v1/docs/",
|
||||
Limit: 0,
|
||||
},
|
||||
}},
|
||||
input: newRequest(t, "GET", "/docs/"),
|
||||
expect: newRequest(t, "GET", "/v1/docs/"),
|
||||
},
|
||||
{
|
||||
rule: Rewrite{PathRegexp: []*regexReplacer{
|
||||
{
|
||||
Find: "/{2,}",
|
||||
Replace: "/",
|
||||
},
|
||||
}},
|
||||
input: newRequest(t, "GET", "/doc//readme.md"),
|
||||
expect: newRequest(t, "GET", "/doc/readme.md"),
|
||||
},
|
||||
} {
|
||||
// copy the original input just enough so that we can
|
||||
// compare it after the rewrite to see if it changed
|
||||
urlCopy := *tc.input.URL
|
||||
originalInput := &http.Request{
|
||||
Method: tc.input.Method,
|
||||
RequestURI: tc.input.RequestURI,
|
||||
URL: &urlCopy,
|
||||
}
|
||||
|
||||
// populate the replacer just enough for our tests
|
||||
repl.Set("http.request.uri", tc.input.RequestURI)
|
||||
repl.Set("http.request.uri.path", tc.input.URL.Path)
|
||||
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
|
||||
|
||||
for _, rep := range tc.rule.PathRegexp {
|
||||
re, err := regexp.Compile(rep.Find)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rep.re = re
|
||||
}
|
||||
|
||||
changed := tc.rule.Rewrite(tc.input, repl)
|
||||
if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {
|
||||
t.Errorf("Test %d: Expected changed=%t but was %t", i, expected, actual)
|
||||
}
|
||||
if tc.rule.StripPathPrefix != "" {
|
||||
t.Logf("Test UriRule \"uri strip_prefix %v\" ==> rewrite \"%v\" to \"%v\"", tc.rule.StripPathPrefix, originalInput.URL, tc.input.URL)
|
||||
} else if tc.rule.StripPathSuffix != "" {
|
||||
t.Logf("Test UriRule \"uri strip_suffix %v\" ==> rewrite \"%v\" to \"%v\"", tc.rule.StripPathSuffix, originalInput.URL, tc.input.URL)
|
||||
} else if tc.rule.URISubstring != nil {
|
||||
t.Logf("Test UriRule \"uri replace %s %s\" ==> rewrite \"%v\" to \"%v\"", tc.rule.URISubstring[0].Find, tc.rule.URISubstring[0].Replace, originalInput.URL, tc.input.URL)
|
||||
} else if tc.rule.PathRegexp != nil {
|
||||
t.Logf("Test UriRule \"uri path_regexp %s %s\" ==> rewrite \"%v\" to \"%v\"", (*tc.rule.PathRegexp[0]).Find, (*tc.rule.PathRegexp[0]).Replace, originalInput.URL, tc.input.URL)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathRewriteRule(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
rr []RewriteRule // not URI-encoded because not parsing from a URI
|
||||
input string // should be valid URI encoding (escaped) since it will become part of a request
|
||||
expect bool
|
||||
provisionErr bool
|
||||
}{
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"* /foo.html",
|
||||
}, WithRewriteFilter),
|
||||
input: "/",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"/api/* ?a=b",
|
||||
}, WithRewriteFilter),
|
||||
input: "/api/abc",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"/api/* ?{query}&a=b",
|
||||
}, WithRewriteFilter),
|
||||
input: "/api/abc",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"* /index.php?{query}&p={path}",
|
||||
}, WithRewriteFilter),
|
||||
input: "/foo/bar",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"/api",
|
||||
}, WithStripPrefixFilter),
|
||||
input: "/api/v1",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
".html",
|
||||
}, WithStripSuffixFilter),
|
||||
input: "/index.html",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"/docs/ /v1/docs/",
|
||||
}, WithReplaceFilter),
|
||||
input: "/docs/go",
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
rr: NewRewriteRulesWithOptions([]string{
|
||||
"/{2,} /",
|
||||
}, WithPathRegexpFilter),
|
||||
input: "/doc//readme.md",
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
u, err := url.ParseRequestURI(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d (%v): Invalid request URI (should be rejected by Go's HTTP server): %v", i, tc.input, err)
|
||||
}
|
||||
req := &http.Request{URL: u}
|
||||
repl := NewReplacer()
|
||||
repl.Set("query", req.URL.RawQuery)
|
||||
repl.Set("path", req.URL.Path)
|
||||
//t.Logf("Init ENV with: {\"query\":\"%v\", \"path\": \"%v\"}", req.URL.RawQuery, req.URL.Path)
|
||||
ctx := context.WithValue(req.Context(), ReplacerCtxKey, repl)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
for _, r := range tc.rr {
|
||||
oldRUL := req.URL.Path
|
||||
actual, err := r.Exec(req)
|
||||
if err != nil {
|
||||
t.Errorf("Test RewriteRule \"rewrite %v %v\" ==> Err %v", r.Match[0], r.Rewrite.URI, err)
|
||||
continue
|
||||
}
|
||||
if actual != tc.expect {
|
||||
t.Errorf("Test RewriteRule \"rewrite %v %v\" ==> Expected %t, got %t for '%s'", r.Match[0], r.Rewrite.URI, tc.expect, actual, tc.input)
|
||||
continue
|
||||
}
|
||||
|
||||
if r.Rewrite.StripPathPrefix != "" {
|
||||
t.Logf("Test RewriteRule \"strip_prefix %v\" ==> rewrite \"%v\" to \"%v\"", r.Rewrite.StripPathPrefix, oldRUL, req.URL)
|
||||
} else if r.Rewrite.StripPathSuffix != "" {
|
||||
t.Logf("Test RewriteRule \"strip_suffix %v\" ==> rewrite \"%v\" to \"%v\"", r.Rewrite.StripPathSuffix, oldRUL, req.URL)
|
||||
} else if r.Rewrite.URISubstring != nil {
|
||||
t.Logf("Test RewriteRule \"replace %s %s\" ==> rewrite \"%v\" to \"%v\"", r.Rewrite.URISubstring[0].Find, r.Rewrite.URISubstring[0].Replace, oldRUL, req.URL)
|
||||
} else if r.Rewrite.PathRegexp != nil {
|
||||
t.Logf("Test RewriteRule \"path_regexp %s %s\" ==> rewrite \"%v\" to \"%v\"", (*r.Rewrite.PathRegexp[0]).Find, (*r.Rewrite.PathRegexp[0]).Replace, oldRUL, req.URL)
|
||||
} else if r.Rewrite.URI != "" {
|
||||
t.Logf("Test RewriteRule \"rewrite %s %s\" ==> rewrite \"%v\" to \"%v\"", r.Match[0], r.Rewrite.URI, oldRUL, req.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newRequest(t *testing.T, method, uri string) *http.Request {
|
||||
req, err := http.NewRequest(method, uri, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating request: %v", err)
|
||||
}
|
||||
req.RequestURI = req.URL.RequestURI() // simulate incoming request
|
||||
return req
|
||||
}
|
||||
|
||||
func reqEqual(r1, r2 *http.Request) bool {
|
||||
if r1.Method != r2.Method {
|
||||
return false
|
||||
}
|
||||
if r1.RequestURI != r2.RequestURI {
|
||||
return false
|
||||
}
|
||||
if (r1.URL == nil && r2.URL != nil) || (r1.URL != nil && r2.URL == nil) {
|
||||
return false
|
||||
}
|
||||
if r1.URL == nil && r2.URL == nil {
|
||||
return true
|
||||
}
|
||||
return r1.URL.Scheme == r2.URL.Scheme &&
|
||||
r1.URL.Host == r2.URL.Host &&
|
||||
r1.URL.Path == r2.URL.Path &&
|
||||
r1.URL.RawPath == r2.URL.RawPath &&
|
||||
r1.URL.RawQuery == r2.URL.RawQuery &&
|
||||
r1.URL.Fragment == r2.URL.Fragment
|
||||
}
|
||||
Reference in New Issue
Block a user