Go API Documentation

github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite

No package summary is available.

Package

Files: 2. Third party imports: 1. Imports from organisation: 0. Tests: 0. Benchmarks: 0.

Vars

Interface guard

var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil)

Types

Rewrite

Rewrite is a middleware which can rewrite/mutate HTTP requests.

The Method and URI properties are "setters" (the request URI will be overwritten with the given values). Other properties are "modifiers" (they modify existing values in a differentiable way). It is atypical to combine the use of setters and modifiers in a single rewrite.

To ensure consistent behavior, prefix and suffix stripping is performed in the URL-decoded (unescaped, normalized) space by default except for the specific bytes where an escape sequence is used in the prefix or suffix pattern.

For all modifiers, paths are cleaned before being modified so that multiple, consecutive slashes are collapsed into a single slash, and dot elements are resolved and removed. In the special case of a prefix, suffix, or substring containing "//" (repeated slashes), slashes will not be merged while cleaning the path so that the rewrite can be interpreted literally.

type Rewrite struct {
	// Changes the request's HTTP verb.
	Method	string	`json:"method,omitempty"`

	// Changes the request's URI, which consists of path and query string.
	// Only components of the URI that are specified will be changed.
	// For example, a value of "/foo.html" or "foo.html" will only change
	// the path and will preserve any existing query string. Similarly, a
	// value of "?a=b" will only change the query string and will not affect
	// the path. Both can also be changed: "/foo?a=b" - this sets both the
	// path and query string at the same time.
	//
	// You can also use placeholders. For example, to preserve the existing
	// query string, you might use: "?{http.request.uri.query}&a=b". Any
	// key-value pairs you add to the query string will not overwrite
	// existing values (individual pairs are append-only).
	//
	// To clear the query string, explicitly set an empty one: "?"
	URI	string	`json:"uri,omitempty"`

	// Strips the given prefix from the beginning of the URI path.
	// The prefix should be written in normalized (unescaped) form,
	// but if an escaping (`%xx`) is used, the path will be required
	// to have that same escape at that position in order to match.
	StripPathPrefix	string	`json:"strip_path_prefix,omitempty"`

	// Strips the given suffix from the end of the URI path.
	// The suffix should be written in normalized (unescaped) form,
	// but if an escaping (`%xx`) is used, the path will be required
	// to have that same escape at that position in order to match.
	StripPathSuffix	string	`json:"strip_path_suffix,omitempty"`

	// Performs substring replacements on the URI.
	URISubstring	[]substrReplacer	`json:"uri_substring,omitempty"`

	// Performs regular expression replacements on the URI path.
	PathRegexp	[]*regexReplacer	`json:"path_regexp,omitempty"`

	// Mutates the query string of the URI.
	Query	*queryOps	`json:"query,omitempty"`

	logger	*zap.Logger
}

queryOps

queryOps describes the operations to perform on query keys: add, set, rename and delete.

type queryOps struct {
	// Renames a query key from Key to Val, without affecting the value.
	Rename	[]queryOpsArguments	`json:"rename,omitempty"`

	// Sets query parameters; overwrites a query key with the given value.
	Set	[]queryOpsArguments	`json:"set,omitempty"`

	// Adds query parameters; does not overwrite an existing query field,
	// and only appends an additional value for that key if any already exist.
	Add	[]queryOpsArguments	`json:"add,omitempty"`

	// Replaces query parameters.
	Replace	[]*queryOpsReplacement	`json:"replace,omitempty"`

	// Deletes a given query key by name.
	Delete	[]string	`json:"delete,omitempty"`
}

queryOpsArguments

This type doesn't have documentation.

type queryOpsArguments struct {
	// A key in the query string. Note that query string keys may appear multiple times.
	Key	string	`json:"key,omitempty"`

	// The value for the given operation; for add and set, this is
	// simply the value of the query, and for rename this is the
	// query key to rename to.
	Val	string	`json:"val,omitempty"`
}

queryOpsReplacement

This type doesn't have documentation.

type queryOpsReplacement struct {
	// The key to replace in the query string.
	Key	string	`json:"key,omitempty"`

	// The substring to search for.
	Search	string	`json:"search,omitempty"`

	// The regular expression to search with.
	SearchRegexp	string	`json:"search_regexp,omitempty"`

	// The string with which to replace matches.
	Replace	string	`json:"replace,omitempty"`

	re	*regexp.Regexp
}

regexReplacer

regexReplacer describes a replacement using a regular expression.

type regexReplacer struct {
	// The regular expression to find.
	Find	string	`json:"find,omitempty"`

	// The substring to replace with. Supports placeholders and
	// regular expression capture groups.
	Replace	string	`json:"replace,omitempty"`

	re	*regexp.Regexp
}

substrReplacer

substrReplacer describes either a simple and fast substring replacement.

type substrReplacer struct {
	// A substring to find. Supports placeholders.
	Find	string	`json:"find,omitempty"`

	// The substring to replace with. Supports placeholders.
	Replace	string	`json:"replace,omitempty"`

	// Maximum number of replacements per string.
	// Set to <= 0 for no limit (default).
	Limit	int	`json:"limit,omitempty"`
}

Functions

func (*Rewrite) Provision

Provision sets up rewr.

func (rewr *Rewrite) Provision(ctx caddy.Context) error {
	rewr.logger = ctx.Logger()

	for i, rep := range rewr.PathRegexp {
		if rep.Find == "" {
			return fmt.Errorf("path_regexp find cannot be empty")
		}
		re, err := regexp.Compile(rep.Find)
		if err != nil {
			return fmt.Errorf("compiling regular expression %d: %v", i, err)
		}
		rep.re = re
	}
	if rewr.Query != nil {
		for _, replacementOp := range rewr.Query.Replace {
			err := replacementOp.Provision(ctx)
			if err != nil {
				return fmt.Errorf("compiling regular expression %s in query rewrite replace operation: %v", replacementOp.SearchRegexp, err)
			}
		}
	}

	return nil
}

Cognitive complexity: 14, Cyclomatic complexity: 7

Uses: fmt.Errorf, regexp.Compile.

func (Rewrite) CaddyModule

CaddyModule returns the Caddy module information.

func (Rewrite) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:	"http.handlers.rewrite",
		New:	func() caddy.Module { return new(Rewrite) },
	}
}

Cognitive complexity: 2, Cyclomatic complexity: 1

func (Rewrite) Rewrite

rewrite performs the rewrites on r using repl, which should have been obtained from r, but is passed in for efficiency. It returns true if any changes were made to r.

func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
	oldMethod := r.Method
	oldURI := r.RequestURI

	// method
	if rewr.Method != "" {
		r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
	}

	// uri (path, query string and... fragment, because why not)
	if uri := rewr.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, r.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 {
				r.URL.Path = newPath
			} else {
				r.URL.Path = path
			}
		}
		if qsStart >= 0 {
			r.URL.RawQuery = newQuery
		}
		if fragStart >= 0 {
			r.URL.Fragment = newFrag
		}
	}

	// strip path prefix or suffix
	if rewr.StripPathPrefix != "" {
		prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
		if !strings.HasPrefix(prefix, "/") {
			prefix = "/" + prefix
		}
		mergeSlashes := !strings.Contains(prefix, "//")
		changePath(r, func(escapedPath string) string {
			escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
			return trimPathPrefix(escapedPath, prefix)
		})
	}
	if rewr.StripPathSuffix != "" {
		suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
		mergeSlashes := !strings.Contains(suffix, "//")
		changePath(r, func(escapedPath string) string {
			escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
			return reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix)))
		})
	}

	// substring replacements in URI
	for _, rep := range rewr.URISubstring {
		rep.do(r, repl)
	}

	// regular expression replacements on the path
	for _, rep := range rewr.PathRegexp {
		rep.do(r, repl)
	}

	// apply query operations
	if rewr.Query != nil {
		rewr.Query.do(r, repl)
	}

	// update the encoded copy of the URI
	r.RequestURI = r.URL.RequestURI()

	// return true if anything changed
	return r.Method != oldMethod || r.RequestURI != oldURI
}

Cognitive complexity: 61, Cyclomatic complexity: 35

Uses: caddyhttp.CleanPath, strings.Contains, strings.Cut, strings.HasPrefix, strings.ReplaceAll, strings.ToUpper, url.PathUnescape.

func (Rewrite) ServeHTTP

func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
	const message = "rewrote request"

	c := rewr.logger.Check(zap.DebugLevel, message)
	if c == nil {
		rewr.Rewrite(r, repl)
		return next.ServeHTTP(w, r)
	}

	changed := rewr.Rewrite(r, repl)

	if changed {
		c.Write(
			zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
			zap.String("method", r.Method),
			zap.String("uri", r.RequestURI),
		)
	}

	return next.ServeHTTP(w, r)
}

Cognitive complexity: 5, Cyclomatic complexity: 3

Uses: caddyhttp.LoggableHTTPRequest, zap.DebugLevel, zap.Object, zap.String.

Private functions

func applyQueryOps

applyQueryOps (h httpcaddyfile.Helper, qo *queryOps, args []string) error
References: strings.Contains, strings.HasPrefix, strings.Split, strings.TrimLeft.

func buildQueryString

buildQueryString takes an input query string and performs replacements on each component, returning the resulting query string. This function appends duplicate keys rather than replaces.

buildQueryString (qs string, repl *caddy.Replacer) string
References: fmt.Sprintf, fmt.Stringer, strconv.Itoa, strings.Builder, strings.Index, url.QueryEscape.

func changePath

changePath (req *http.Request, newVal func(pathOrRawPath string) string)
References: url.PathUnescape.

func init

init ()
References: httpcaddyfile.RegisterDirective, httpcaddyfile.RegisterHandlerDirective.

func parseCaddyfileHandlePath

parseCaddyfileHandlePath parses the handle_path directive. Syntax:

handle_path [<matcher>] {
    <directives...>
}

Only path matchers (with a / prefix) are supported as this is a shortcut for the handle directive with a strip_prefix rewrite.

parseCaddyfileHandlePath (h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
References: caddyconfig.JSONModuleObject, caddyhttp.MatchPath, caddyhttp.Route, caddyhttp.Subroute, httpcaddyfile.ParseSegmentAsSubroute, json.RawMessage, strings.HasPrefix, strings.HasSuffix.

func parseCaddyfileMethod

parseCaddyfileMethod sets up a basic method rewrite handler from Caddyfile tokens. Syntax:

method [<matcher>] <method>

parseCaddyfileMethod (h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)

func parseCaddyfileRewrite

parseCaddyfileRewrite sets up a basic rewrite handler from Caddyfile tokens. Syntax:

rewrite [<matcher>] <to>

Only URI components which are given in <to> will be set in the resulting URI. See the docs for the rewrite handler for more information.

parseCaddyfileRewrite (h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)

func parseCaddyfileURI

parseCaddyfileURI sets up a handler for manipulating (but not "rewriting") the URI from Caddyfile tokens. Syntax:

uri [<matcher>] strip_prefix|strip_suffix|replace|path_regexp <target> [<replacement> [<limit>]]

If strip_prefix or strip_suffix are used, then <target> will be stripped only if it is the beginning or the end, respectively, of the URI path. If replace is used, then <target> will be replaced with <replacement> across the whole URI, up to <limit> times (or unlimited if unspecified). If path_regexp is used, then regular expression replacements will be performed on the path portion of the URI (and a limit cannot be set).

parseCaddyfileURI (h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
References: strconv.Atoi.

func reverse

reverse (s string) string

func trimPathPrefix

trimPathPrefix is like strings.TrimPrefix, but customized for advanced URI path prefix matching. The string prefix will be trimmed from the beginning of escapedPath if escapedPath starts with prefix. Rather than a naive 1:1 comparison of each byte to determine if escapedPath starts with prefix, both strings are iterated in lock-step, and if prefix has a '%' encoding at a particular position, escapedPath must also have the same encoding representation for that character. In other words, if the prefix string uses the escaped form for a character, escapedPath must literally use the same escape at that position. Otherwise, all character comparisons are performed in normalized/unescaped space.

trimPathPrefix (escapedPath,prefix string) string
References: strings.EqualFold, url.PathUnescape.

func do

do performs the substring replacement on r.

do (r *http.Request, repl *caddy.Replacer)
References: caddyhttp.CleanPath, strings.Contains, strings.Replace.


Tests

Files: 1. Third party imports: 0. Imports from organisation: 0. Tests: 1. Benchmarks: 0.

Test functions

TestRewrite

References: http.Request, regexp.Compile.