1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
|
// 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 rewrite
import (
"encoding/json"
"strconv"
"strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
func init() {
httpcaddyfile.RegisterDirective("rewrite", parseCaddyfileRewrite)
httpcaddyfile.RegisterHandlerDirective("method", parseCaddyfileMethod)
httpcaddyfile.RegisterHandlerDirective("uri", parseCaddyfileURI)
httpcaddyfile.RegisterDirective("handle_path", parseCaddyfileHandlePath)
}
// 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.
func parseCaddyfileRewrite(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
h.Next() // consume directive name
// count the tokens to determine what to do
argsCount := h.CountRemainingArgs()
if argsCount == 0 {
return nil, h.Errf("too few arguments; must have at least a rewrite URI")
}
if argsCount > 2 {
return nil, h.Errf("too many arguments; should only be a matcher and a URI")
}
// with only one arg, assume it's a rewrite URI with no matcher token
if argsCount == 1 {
if !h.NextArg() {
return nil, h.ArgErr()
}
return h.NewRoute(nil, Rewrite{URI: h.Val()}), nil
}
// parse the matcher token into a matcher set
userMatcherSet, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
h.Next() // consume directive name again, matcher parsing does a reset
h.Next() // advance to the rewrite URI
return h.NewRoute(userMatcherSet, Rewrite{URI: h.Val()}), nil
}
// parseCaddyfileMethod sets up a basic method rewrite handler from Caddyfile tokens. Syntax:
//
// method [<matcher>] <method>
func parseCaddyfileMethod(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive name
if !h.NextArg() {
return nil, h.ArgErr()
}
if h.NextArg() {
return nil, h.ArgErr()
}
return Rewrite{Method: h.Val()}, nil
}
// 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).
func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
h.Next() // consume directive name
args := h.RemainingArgs()
if len(args) < 1 {
return nil, h.ArgErr()
}
var rewr Rewrite
switch args[0] {
case "strip_prefix":
if len(args) != 2 {
return nil, h.ArgErr()
}
rewr.StripPathPrefix = args[1]
if !strings.HasPrefix(rewr.StripPathPrefix, "/") {
rewr.StripPathPrefix = "/" + rewr.StripPathPrefix
}
case "strip_suffix":
if len(args) != 2 {
return nil, h.ArgErr()
}
rewr.StripPathSuffix = args[1]
case "replace":
var find, replace, lim string
switch len(args) {
case 4:
lim = args[3]
fallthrough
case 3:
find = args[1]
replace = args[2]
default:
return nil, h.ArgErr()
}
var limInt int
if lim != "" {
var err error
limInt, err = strconv.Atoi(lim)
if err != nil {
return nil, h.Errf("limit must be an integer; invalid: %v", err)
}
}
rewr.URISubstring = append(rewr.URISubstring, substrReplacer{
Find: find,
Replace: replace,
Limit: limInt,
})
case "path_regexp":
if len(args) != 3 {
return nil, h.ArgErr()
}
find, replace := args[1], args[2]
rewr.PathRegexp = append(rewr.PathRegexp, ®exReplacer{
Find: find,
Replace: replace,
})
case "query":
if len(args) > 4 {
return nil, h.ArgErr()
}
rewr.Query = &queryOps{}
var hasArgs bool
if len(args) > 1 {
hasArgs = true
err := applyQueryOps(h, rewr.Query, args[1:])
if err != nil {
return nil, err
}
}
for h.NextBlock(0) {
if hasArgs {
return nil, h.Err("Cannot specify uri query rewrites in both argument and block")
}
queryArgs := []string{h.Val()}
queryArgs = append(queryArgs, h.RemainingArgs()...)
err := applyQueryOps(h, rewr.Query, queryArgs)
if err != nil {
return nil, err
}
}
default:
return nil, h.Errf("unrecognized URI manipulation '%s'", args[0])
}
return rewr, nil
}
func applyQueryOps(h httpcaddyfile.Helper, qo *queryOps, args []string) error {
key := args[0]
switch {
case strings.HasPrefix(key, "-"):
if len(args) != 1 {
return h.ArgErr()
}
qo.Delete = append(qo.Delete, strings.TrimLeft(key, "-"))
case strings.HasPrefix(key, "+"):
if len(args) != 2 {
return h.ArgErr()
}
param := strings.TrimLeft(key, "+")
qo.Add = append(qo.Add, queryOpsArguments{Key: param, Val: args[1]})
case strings.Contains(key, ">"):
if len(args) != 1 {
return h.ArgErr()
}
renameValKey := strings.Split(key, ">")
qo.Rename = append(qo.Rename, queryOpsArguments{Key: renameValKey[0], Val: renameValKey[1]})
case len(args) == 3:
qo.Replace = append(qo.Replace, &queryOpsReplacement{Key: key, SearchRegexp: args[1], Replace: args[2]})
default:
if len(args) != 2 {
return h.ArgErr()
}
qo.Set = append(qo.Set, queryOpsArguments{Key: key, Val: args[1]})
}
return nil
}
// 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.
func parseCaddyfileHandlePath(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
h.Next() // consume directive name
// there must be a path matcher
if !h.NextArg() {
return nil, h.ArgErr()
}
// read the prefix to strip
path := h.Val()
if !strings.HasPrefix(path, "/") {
return nil, h.Errf("path matcher must begin with '/', got %s", path)
}
// we only want to strip what comes before the '/' if
// the user specified it (e.g. /api/* should only strip /api)
var stripPath string
if strings.HasSuffix(path, "/*") {
stripPath = path[:len(path)-2]
} else if strings.HasSuffix(path, "*") {
stripPath = path[:len(path)-1]
} else {
stripPath = path
}
// the ParseSegmentAsSubroute function expects the cursor
// to be at the token just before the block opening,
// so we need to rewind because we already read past it
h.Reset()
h.Next()
// parse the block contents as a subroute handler
handler, err := httpcaddyfile.ParseSegmentAsSubroute(h)
if err != nil {
return nil, err
}
subroute, ok := handler.(*caddyhttp.Subroute)
if !ok {
return nil, h.Errf("segment was not parsed as a subroute")
}
// make a matcher on the path and everything below it
pathMatcher := caddy.ModuleMap{
"path": h.JSON(caddyhttp.MatchPath{path}),
}
// build a route with a rewrite handler to strip the path prefix
route := caddyhttp.Route{
HandlersRaw: []json.RawMessage{
caddyconfig.JSONModuleObject(Rewrite{
StripPathPrefix: stripPath,
}, "handler", "rewrite", nil),
},
}
// prepend the route to the subroute
subroute.Routes = append([]caddyhttp.Route{route}, subroute.Routes...)
// build and return a route from the subroute
return h.NewRoute(pathMatcher, subroute), nil
}
|