Files
kubesphere/pkg/utils/directives/rewrite.go
KubeSphere CI Bot 447a51f08b 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>
2024-09-06 11:05:52 +08:00

473 lines
12 KiB
Go

/*
* 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
}