Skip to content

Commit

Permalink
caddyhttp: Add 'map' handler (#3199)
Browse files Browse the repository at this point in the history
* inital map implementation

* resolve the value during middleware execution

* use regex instead

* pr feedback

* renamed mmap to maphandler

* refactored GetString implementation

* fixed mispelling

* additional feedback
  • Loading branch information
sarge authored Jun 26, 2020
1 parent caca55e commit 6004d3f
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 0 deletions.
1 change: 1 addition & 0 deletions caddyconfig/httpcaddyfile/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
// The header directive goes second so that headers
// can be manipulated before doing redirects.
var directiveOrder = []string{
"map",
"root",

"header",
Expand Down
143 changes: 143 additions & 0 deletions caddytest/integration/map_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package integration

import (
"bytes"
"testing"

"github.com/caddyserver/caddy/v2/caddytest"
)

func TestMap(t *testing.T) {

// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
http_port 9080
https_port 9443
}
localhost:9080 {
map http.request.method dest-name {
default unknown
G.T get-called
POST post-called
}
respond /version 200 {
body "hello from localhost {dest-name}"
}
}
`, "caddyfile")

// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
}

func TestMapRespondWithDefault(t *testing.T) {

// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
http_port 9080
https_port 9443
}
localhost:9080 {
map http.request.method dest-name {
default unknown
GET get-called
}
respond /version 200 {
body "hello from localhost {dest-name}"
}
}
`, "caddyfile")

// act and assert
tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost unknown")
}

func TestMapAsJson(t *testing.T) {

// arrange
tester := caddytest.NewTester(t)
tester.InitServer(`{
"apps": {
"http": {
"http_port": 9080,
"https_port": 9443,
"servers": {
"srv0": {
"listen": [
":9080"
],
"routes": [
{
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "map",
"source": "http.request.method",
"destination": "dest-name",
"default": "unknown",
"items": [
{
"expression": "GET",
"value": "get-called"
},
{
"expression": "POST",
"value": "post-called"
}
]
}
]
},
{
"handle": [
{
"body": "hello from localhost {dest-name}",
"handler": "static_response",
"status_code": 200
}
],
"match": [
{
"path": [
"/version"
]
}
]
}
]
}
],
"match": [
{
"host": [
"localhost"
]
}
],
"terminal": true
}
]
}
}
}
}
}
`, "json")

tester.AssertGetResponse("http://localhost:9080/version", 200, "hello from localhost get-called")
tester.AssertPostResponseBody("http://localhost:9080/version", []string{}, bytes.NewBuffer([]byte{}), 200, "hello from localhost post-called")
}
71 changes: 71 additions & 0 deletions modules/caddyhttp/map/caddyfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package maphandler

import (
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func init() {
httpcaddyfile.RegisterHandlerDirective("map", parseCaddyfile)
}

// parseCaddyfile sets up the handler for a map from Caddyfile tokens. Syntax:
//
// map <source> <dest> {
// [default <default>] - used if not match is found
// [<regexp> <replacement>] - regular expression to match against the source find and the matching replacement value
// ...
// }
//
// The map takes a source variable and maps it into the dest variable. The mapping process
// will check the source variable for the first successful match against a list of regular expressions.
// If a successful match is found the dest variable will contain the replacement value.
// If no successful match is found and the default is specified then the dest will contain the default value.
//
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
m := new(Handler)

for h.Next() {
// first see if source and dest are configured
if h.NextArg() {
m.Source = h.Val()
if h.NextArg() {
m.Destination = h.Val()
}
}

// load the rules
for h.NextBlock(0) {
expression := h.Val()
if expression == "default" {
args := h.RemainingArgs()
if len(args) != 1 {
return m, h.ArgErr()
}
m.Default = args[0]
} else {
args := h.RemainingArgs()
if len(args) != 1 {
return m, h.ArgErr()
}
m.Items = append(m.Items, Item{Expression: expression, Value: args[0]})
}
}
}

return m, nil
}
105 changes: 105 additions & 0 deletions modules/caddyhttp/map/map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package maphandler

import (
"net/http"
"regexp"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)

func init() {
caddy.RegisterModule(Handler{})
}

// Handler is a middleware that maps a source placeholder to a destination
// placeholder.
//
// The mapping process happens early in the request handling lifecycle so that
// the Destination placeholder is calculated and available for substitution.
// The Items array contains pairs of regex expressions and values, the
// Source is matched against the expression, if they match then the destination
// placeholder is set to the value.
//
// The Default is optional, if no Item expression is matched then the value of
// the Default will be used.
//
type Handler struct {
// Source is a placeholder
Source string `json:"source,omitempty"`
// Destination is a new placeholder
Destination string `json:"destination,omitempty"`
// Default is an optional value to use if no other was found
Default string `json:"default,omitempty"`
// Items is an array of regex expressions and values
Items []Item `json:"items,omitempty"`
}

// CaddyModule returns the Caddy module information.
func (Handler) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.map",
New: func() caddy.Module { return new(Handler) },
}
}

// Provision will compile all regular expressions
func (h *Handler) Provision(_ caddy.Context) error {
for i := 0; i < len(h.Items); i++ {
h.Items[i].compiled = regexp.MustCompile(h.Items[i].Expression)
}
return nil
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

// get the source value, if the source value was not found do no
// replacement.
val, ok := repl.GetString(h.Source)
if ok {
found := false
for i := 0; i < len(h.Items); i++ {
if h.Items[i].compiled.MatchString(val) {
found = true
repl.Set(h.Destination, h.Items[i].Value)
break
}
}

if !found && h.Default != "" {
repl.Set(h.Destination, h.Default)
}
}
return next.ServeHTTP(w, r)
}

// Item defines each entry in the map
type Item struct {
// Expression is the regular expression searched for
Expression string `json:"expression,omitempty"`
// Value to use once the expression has been found
Value string `json:"value,omitempty"`
// compiled expression, internal use
compiled *regexp.Regexp
}

// Interface guards
var (
_ caddy.Provisioner = (*Handler)(nil)
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
)
1 change: 1 addition & 0 deletions modules/caddyhttp/standard/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode/zstd"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/map"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/requestbody"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
Expand Down
7 changes: 7 additions & 0 deletions replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ func (r *Replacer) Get(variable string) (interface{}, bool) {
return nil, false
}

// GetString is the same as Get, but coerces the value to a
// string representation.
func (r *Replacer) GetString(variable string) (string, bool) {
s, found := r.Get(variable)
return toString(s), found
}

// Delete removes a variable with a static value
// that was created using Set.
func (r *Replacer) Delete(variable string) {
Expand Down

0 comments on commit 6004d3f

Please sign in to comment.