@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 02:15 UTC

master
go 297 lines 9.41 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package forwardauth
16
17 import (
18 "encoding/json"
19 "net/http"
20 "sort"
21 "strings"
22
23 "github.com/caddyserver/caddy/v2"
24 "github.com/caddyserver/caddy/v2/caddyconfig"
25 "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
26 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
28 "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
29 "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
30 )
31
32 func init() {
33 httpcaddyfile.RegisterDirective("forward_auth", parseCaddyfile)
34 }
35
36 // parseCaddyfile parses the forward_auth directive, which has the same syntax
37 // as the reverse_proxy directive (in fact, the reverse_proxy's directive
38 // Unmarshaler is invoked by this function) but the resulting proxy is specially
39 // configured for most™️ auth gateways that support forward auth. The typical
40 // config which looks something like this:
41 //
42 // forward_auth auth-gateway:9091 {
43 // uri /authenticate?redirect=https://auth.example.com
44 // copy_headers Remote-User Remote-Email
45 // }
46 //
47 // is equivalent to a reverse_proxy directive like this:
48 //
49 // reverse_proxy auth-gateway:9091 {
50 // method GET
51 // rewrite /authenticate?redirect=https://auth.example.com
52 //
53 // header_up X-Forwarded-Method {method}
54 // header_up X-Forwarded-Uri {uri}
55 //
56 // @good status 2xx
57 // handle_response @good {
58 // request_header {
59 // Remote-User {http.reverse_proxy.header.Remote-User}
60 // Remote-Email {http.reverse_proxy.header.Remote-Email}
61 // }
62 // }
63 // }
64 func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
65 if !h.Next() {
66 return nil, h.ArgErr()
67 }
68
69 // if the user specified a matcher token, use that
70 // matcher in a route that wraps both of our routes;
71 // either way, strip the matcher token and pass
72 // the remaining tokens to the unmarshaler so that
73 // we can gain the rest of the reverse_proxy syntax
74 userMatcherSet, err := h.ExtractMatcherSet()
75 if err != nil {
76 return nil, err
77 }
78
79 // make a new dispenser from the remaining tokens so that we
80 // can reset the dispenser back to this point for the
81 // reverse_proxy unmarshaler to read from it as well
82 dispenser := h.NewFromNextSegment()
83
84 // create the reverse proxy handler
85 rpHandler := &reverseproxy.Handler{
86 // set up defaults for header_up; reverse_proxy already deals with
87 // adding the other three X-Forwarded-* headers, but for this flow,
88 // we want to also send along the incoming method and URI since this
89 // request will have a rewritten URI and method.
90 Headers: &headers.Handler{
91 Request: &headers.HeaderOps{
92 Set: http.Header{
93 "X-Forwarded-Method": []string{"{http.request.method}"},
94 "X-Forwarded-Uri": []string{"{http.request.uri}"},
95 },
96 },
97 },
98
99 // we always rewrite the method to GET, which implicitly
100 // turns off sending the incoming request's body, which
101 // allows later middleware handlers to consume it
102 Rewrite: &rewrite.Rewrite{
103 Method: "GET",
104 },
105
106 HandleResponse: []caddyhttp.ResponseHandler{},
107 }
108
109 // collect the headers to copy from the auth response
110 // onto the original request, so they can get passed
111 // through to a backend app
112 headersToCopy := make(map[string]string)
113
114 // read the subdirectives for configuring the forward_auth shortcut
115 // NOTE: we delete the tokens as we go so that the reverse_proxy
116 // unmarshal doesn't see these subdirectives which it cannot handle
117 for dispenser.Next() {
118 for dispenser.NextBlock(0) {
119 // ignore any sub-subdirectives that might
120 // have the same name somewhere within
121 // the reverse_proxy passthrough tokens
122 if dispenser.Nesting() != 1 {
123 continue
124 }
125
126 // parse the forward_auth subdirectives
127 switch dispenser.Val() {
128 case "uri":
129 if !dispenser.NextArg() {
130 return nil, dispenser.ArgErr()
131 }
132 uri := dispenser.Val()
133 if rpHandler.Rewrite.URI != "" {
134 return nil, dispenser.Errf("cannot re-declare uri: %s", uri)
135 }
136 rpHandler.Rewrite.URI = uri
137 dispenser.DeleteN(2)
138
139 case "copy_headers":
140 args := dispenser.RemainingArgs()
141 hadBlock := false
142 for nesting := dispenser.Nesting(); dispenser.NextBlock(nesting); {
143 hadBlock = true
144 args = append(args, dispenser.Val())
145 }
146
147 // directive name + args
148 dispenser.DeleteN(len(args) + 1)
149 if hadBlock {
150 // opening & closing brace
151 dispenser.DeleteN(2)
152 }
153
154 for _, headerField := range args {
155 if strings.Contains(headerField, ">") {
156 parts := strings.Split(headerField, ">")
157 headersToCopy[parts[0]] = parts[1]
158 } else {
159 headersToCopy[headerField] = headerField
160 }
161 }
162 if len(headersToCopy) == 0 {
163 return nil, dispenser.ArgErr()
164 }
165 }
166 }
167 }
168
169 // reset the dispenser after we're done so that the reverse_proxy
170 // unmarshaler can read it from the start
171 dispenser.Reset()
172
173 // the auth target URI must not be empty
174 if rpHandler.Rewrite.URI == "" {
175 return nil, dispenser.Errf("the 'uri' subdirective is required")
176 }
177
178 // Set up handler for good responses; when a response has 2xx status,
179 // then we will copy some headers from the response onto the original
180 // request, and allow handling to continue down the middleware chain,
181 // by _not_ executing a terminal handler. We must have at least one
182 // route in the response handler, even if it's no-op, so that the
183 // response handling logic in reverse_proxy doesn't skip this entry.
184 goodResponseHandler := caddyhttp.ResponseHandler{
185 Match: &caddyhttp.ResponseMatcher{
186 StatusCode: []int{2},
187 },
188 Routes: []caddyhttp.Route{
189 {
190 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
191 &caddyhttp.VarsMiddleware{},
192 "handler",
193 "vars",
194 nil,
195 )},
196 },
197 },
198 }
199
200 // Sort the headers so that the order in the JSON output is deterministic.
201 sortedHeadersToCopy := make([]string, 0, len(headersToCopy))
202 for k := range headersToCopy {
203 sortedHeadersToCopy = append(sortedHeadersToCopy, k)
204 }
205 sort.Strings(sortedHeadersToCopy)
206
207 // Set up handlers to copy headers from the auth response onto the
208 // original request. We use vars matchers to test that the placeholder
209 // values aren't empty, because the header handler would not replace
210 // placeholders which have no value.
211 copyHeaderRoutes := []caddyhttp.Route{}
212 for _, from := range sortedHeadersToCopy {
213 to := http.CanonicalHeaderKey(headersToCopy[from])
214 placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)
215
216 // Always delete the client-supplied header before conditionally setting
217 // it from the auth response. Without this, a client that pre-supplies a
218 // header listed in copy_headers can inject arbitrary values when the auth
219 // service does not return that header: the MatchNot guard below would
220 // skip the Set entirely, leaving the original client-controlled value
221 // intact and forwarding it to the backend.
222 copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
223 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
224 &headers.Handler{
225 Request: &headers.HeaderOps{
226 Delete: []string{to},
227 },
228 },
229 "handler", "headers", nil,
230 )},
231 })
232
233 handler := &headers.Handler{
234 Request: &headers.HeaderOps{
235 Set: http.Header{
236 to: []string{"{" + placeholderName + "}"},
237 },
238 },
239 }
240 copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
241 MatcherSetsRaw: []caddy.ModuleMap{{
242 "not": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{
243 "vars": h.JSON(caddyhttp.VarsMatcher{"{" + placeholderName + "}": []string{""}}),
244 }}}),
245 }},
246 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
247 handler,
248 "handler",
249 "headers",
250 nil,
251 )},
252 })
253 }
254
255 goodResponseHandler.Routes = append(goodResponseHandler.Routes, copyHeaderRoutes...)
256
257 // note that when a response has any other status than 2xx, then we
258 // use the reverse proxy's default behaviour of copying the response
259 // back to the client, so we don't need to explicitly add a response
260 // handler specifically for that behaviour; we do need the 2xx handler
261 // though, to make handling fall through to handlers deeper in the chain.
262 rpHandler.HandleResponse = append(rpHandler.HandleResponse, goodResponseHandler)
263
264 // the rest of the config is specified by the user
265 // using the reverse_proxy directive syntax
266 dispenser.Next() // consume the directive name
267 err = rpHandler.UnmarshalCaddyfile(dispenser)
268 if err != nil {
269 return nil, err
270 }
271 err = rpHandler.FinalizeUnmarshalCaddyfile(h)
272 if err != nil {
273 return nil, err
274 }
275
276 // create the final reverse proxy route
277 rpRoute := caddyhttp.Route{
278 HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
279 rpHandler,
280 "handler",
281 "reverse_proxy",
282 nil,
283 )},
284 }
285
286 // apply the user's matcher if any
287 if userMatcherSet != nil {
288 rpRoute.MatcherSetsRaw = []caddy.ModuleMap{userMatcherSet}
289 }
290
291 return []httpcaddyfile.ConfigValue{
292 {
293 Class: "route",
294 Value: rpRoute,
295 },
296 }, nil
297 }