Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

caddyhttp: Add map handler #3199

Merged
merged 9 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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