15
package headers
16
17
import (
18
+ "fmt"
19
"net/http"
20
+ "reflect"
21
"strings"
22
23
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
25
)
26
27
func init() {
26
- httpcaddyfile.RegisterHandlerDirective("header", parseCaddyfile)
27
- httpcaddyfile.RegisterHandlerDirective("request_header", parseReqHdrCaddyfile)
28
+ httpcaddyfile.RegisterDirective("header", parseCaddyfile)
29
+ httpcaddyfile.RegisterDirective("request_header", parseReqHdrCaddyfile)
30
}
31
32
// parseCaddyfile sets up the handler for response headers from
33
// Caddyfile tokens. Syntax:
34
//
33
-// header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]] {
35
+// header [<matcher>] [[+|-|?]<field> [<value|regexp>] [<replacement>]] {
36
// [+]<field> [<value|regexp> [<replacement>]]
37
+// ?<field> <default_value>
38
// -<field>
39
// [defer]
40
// }
42
// Either a block can be opened or a single header field can be configured
43
// in the first line, but not both in the same directive. Header operations
44
// are deferred to write-time if any headers are being deleted or if the
42
-// 'defer' subdirective is used.
43
-func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
44
- hdr := new(Handler)
45
-
46
- makeResponseOps := func() {
47
- if hdr.Response == nil {
48
- hdr.Response = &RespHeaderOps{
49
- HeaderOps: new(HeaderOps),
50
- }
45
+// 'defer' subdirective is used. + appends a header value, - deletes a field,
46
+// and ? conditionally sets a value only if the header field is not already
47
+// set.
48
+func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
49
+ matcherSet, err := h.ExtractMatcherSet()
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+
54
+ makeHandler := func() Handler {
55
+ return Handler{
56
+ Response: &RespHeaderOps{
57
+ HeaderOps: &HeaderOps{},
58
+ },
59
}
60
}
61
+ handler, handlerWithRequire := makeHandler(), makeHandler()
62
63
for h.Next() {
64
// first see if headers are in the initial line
73
if h.NextArg() {
74
replacement = h.Val()
75
}
67
- makeResponseOps()
68
- CaddyfileHeaderOp(hdr.Response.HeaderOps, field, value, replacement)
69
- if len(hdr.Response.HeaderOps.Delete) > 0 {
70
- hdr.Response.Deferred = true
76
+ err := applyHeaderOp(
77
+ handler.Response.HeaderOps,
78
+ handler.Response,
79
+ field,
80
+ value,
81
+ replacement,
82
+ )
83
+ if err != nil {
84
+ return nil, h.Err(err.Error())
85
+ }
86
+ if len(handler.Response.HeaderOps.Delete) > 0 {
87
+ handler.Response.Deferred = true
88
}
89
}
90
92
for h.NextBlock(0) {
93
field := h.Val()
94
if field == "defer" {
78
- hdr.Response.Deferred = true
95
+ handler.Response.Deferred = true
96
continue
97
}
98
if hasArgs {
82
- return nil, h.Err("cannot specify headers in both arguments and block")
99
+ return nil, h.Err("cannot specify headers in both arguments and block") // because it would be weird
100
}
101
+
102
+ // sometimes it is habitual for users to suffix a field name with a colon,
103
+ // as if they were writing a curl command or something; see
104
+ // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19
105
+ field = strings.TrimSuffix(field, ":")
106
+
107
var value, replacement string
108
if h.NextArg() {
109
value = h.Val()
111
if h.NextArg() {
112
replacement = h.Val()
113
}
91
- makeResponseOps()
92
- CaddyfileHeaderOp(hdr.Response.HeaderOps, field, value, replacement)
93
- if len(hdr.Response.HeaderOps.Delete) > 0 {
94
- hdr.Response.Deferred = true
114
+
115
+ handlerToUse := handler
116
+ if strings.HasPrefix(field, "?") {
117
+ handlerToUse = handlerWithRequire
118
+ }
119
+
120
+ err := applyHeaderOp(
121
+ handlerToUse.Response.HeaderOps,
122
+ handlerToUse.Response,
123
+ field,
124
+ value,
125
+ replacement,
126
+ )
127
+ if err != nil {
128
+ return nil, h.Err(err.Error())
129
}
130
}
131
}
132
99
- return hdr, nil
133
+ var configValues []httpcaddyfile.ConfigValue
134
+ if !reflect.DeepEqual(handler, makeHandler()) {
135
+ configValues = append(configValues, h.NewRoute(matcherSet, handler)...)
136
+ }
137
+ if !reflect.DeepEqual(handlerWithRequire, makeHandler()) {
138
+ configValues = append(configValues, h.NewRoute(matcherSet, handlerWithRequire)...)
139
+ }
140
+
141
+ return configValues, nil
142
}
143
144
// parseReqHdrCaddyfile sets up the handler for request headers
146
//
147
// request_header [<matcher>] [[+|-]<field> [<value|regexp>] [<replacement>]]
148
//
107
-func parseReqHdrCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
108
- hdr := new(Handler)
149
+func parseReqHdrCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
150
+ matcherSet, err := h.ExtractMatcherSet()
151
+ if err != nil {
152
+ return nil, err
153
+ }
154
+
155
+ configValues := []httpcaddyfile.ConfigValue{}
156
+
157
for h.Next() {
158
if !h.NextArg() {
159
return nil, h.ArgErr()
160
}
161
field := h.Val()
162
163
+ hdr := Handler{
164
+ Request: &HeaderOps{},
165
+ }
166
+
167
// sometimes it is habitual for users to suffix a field name with a colon,
168
// as if they were writing a curl command or something; see
117
- // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349
169
+ // https://caddy.community/t/v2-reverse-proxy-please-add-cors-example-to-the-docs/7349/19
170
field = strings.TrimSuffix(field, ":")
171
172
var value, replacement string
183
if hdr.Request == nil {
184
hdr.Request = new(HeaderOps)
185
}
134
- CaddyfileHeaderOp(hdr.Request, field, value, replacement)
186
+ if err := CaddyfileHeaderOp(hdr.Request, field, value, replacement); err != nil {
187
+ return nil, h.Err(err.Error())
188
+ }
189
+
190
+ configValues = append(configValues, h.NewRoute(matcherSet, hdr)...)
191
192
if h.NextArg() {
193
return nil, h.ArgErr()
194
}
195
}
140
- return hdr, nil
196
+ return configValues, nil
197
}
198
199
// CaddyfileHeaderOp applies a new header operation according to
204
// will be used to search and then replacement will be used to
205
// complete the substring replacement; in that case, any + or -
206
// prefix to field will be ignored.
151
-func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) {
152
- if strings.HasPrefix(field, "+") {
207
+func CaddyfileHeaderOp(ops *HeaderOps, field, value, replacement string) error {
208
+ return applyHeaderOp(ops, nil, field, value, replacement)
209
+}
210
+
211
+func applyHeaderOp(ops *HeaderOps, respHeaderOps *RespHeaderOps, field, value, replacement string) error {
212
+ switch {
213
+ case strings.HasPrefix(field, "+"): // append
214
if ops.Add == nil {
215
ops.Add = make(http.Header)
216
}
217
ops.Add.Set(field[1:], value)
157
- } else if strings.HasPrefix(field, "-") {
218
+
219
+ case strings.HasPrefix(field, "-"): // delete
220
ops.Delete = append(ops.Delete, field[1:])
159
- } else {
160
- if replacement == "" {
161
- if ops.Set == nil {
162
- ops.Set = make(http.Header)
163
- }
164
- ops.Set.Set(field, value)
165
- } else {
166
- if ops.Replace == nil {
167
- ops.Replace = make(map[string][]Replacement)
221
+ if respHeaderOps != nil {
222
+ respHeaderOps.Deferred = true
223
+ }
224
+
225
+ case strings.HasPrefix(field, "?"): // default (conditional on not existing) - response headers only
226
+ if respHeaderOps == nil {
227
+ return fmt.Errorf("%v: the default header modifier ('?') can only be used on response headers; for conditional manipulation of request headers, use matchers", field)
228
+ }
229
+ if respHeaderOps.Require == nil {
230
+ respHeaderOps.Require = &caddyhttp.ResponseMatcher{
231
+ Headers: make(http.Header),
232
}
169
- field = strings.TrimLeft(field, "+-")
170
- ops.Replace[field] = append(
171
- ops.Replace[field],
172
- Replacement{
173
- SearchRegexp: value,
174
- Replace: replacement,
175
- },
176
- )
233
}
234
+ field = strings.TrimPrefix(field, "?")
235
+ respHeaderOps.Require.Headers[field] = nil
236
+ if respHeaderOps.Set == nil {
237
+ respHeaderOps.Set = make(http.Header)
238
+ }
239
+ respHeaderOps.Set.Set(field, value)
240
+
241
+ case replacement != "": // replace
242
+ if ops.Replace == nil {
243
+ ops.Replace = make(map[string][]Replacement)
244
+ }
245
+ field = strings.TrimLeft(field, "+-?")
246
+ ops.Replace[field] = append(
247
+ ops.Replace[field],
248
+ Replacement{
249
+ SearchRegexp: value,
250
+ Replace: replacement,
251
+ },
252
+ )
253
+
254
+ default: // set (overwrite)
255
+ if ops.Set == nil {
256
+ ops.Set = make(http.Header)
257
+ }
258
+ ops.Set.Set(field, value)
259
}
260
+
261
+ return nil
262
}