@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, 01:00 UTC

master
go 466 lines 13.8 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 caddyhttp
16
17 import (
18 "bytes"
19 "encoding/json"
20 "fmt"
21 "io"
22 "net/http"
23 "net/textproto"
24 "os"
25 "slices"
26 "strconv"
27 "strings"
28 "text/template"
29 "time"
30
31 "github.com/spf13/cobra"
32 "go.uber.org/zap"
33
34 caddycmd "github.com/caddyserver/caddy/v2/cmd"
35
36 "github.com/caddyserver/caddy/v2"
37 "github.com/caddyserver/caddy/v2/caddyconfig"
38 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
39 )
40
41 func init() {
42 caddy.RegisterModule(StaticResponse{})
43 caddycmd.RegisterCommand(caddycmd.Command{
44 Name: "respond",
45 Usage: `[--status <code>] [--body <content>] [--listen <addr>] [--access-log] [--debug] [--header "Field: value"] <body|status>`,
46 Short: "Simple, hard-coded HTTP responses for development and testing",
47 Long: `
48 Spins up a quick-and-clean HTTP server for development and testing purposes.
49
50 With no options specified, this command listens on a random available port
51 and answers HTTP requests with an empty 200 response. The listen address can
52 be customized with the --listen flag and will always be printed to stdout.
53 If the listen address includes a port range, multiple servers will be started.
54
55 If a final, unnamed argument is given, it will be treated as a status code
56 (same as the --status flag) if it is a 3-digit number. Otherwise, it is used
57 as the response body (same as the --body flag). The --status and --body flags
58 will always override this argument (for example, to write a body that
59 literally says "404" but with a status code of 200, do '--status 200 404').
60
61 A body may be given in 3 ways: a flag, a final (and unnamed) argument to
62 the command, or piped to stdin (if flag and argument are unset). Limited
63 template evaluation is supported on the body, with the following variables:
64
65 {{.N}} The server number (useful if using a port range)
66 {{.Port}} The listener port
67 {{.Address}} The listener address
68
69 (See the docs for the text/template package in the Go standard library for
70 information about using templates: https://pkg.go.dev/text/template)
71
72 Access/request logging and more verbose debug logging can also be enabled.
73
74 Response headers may be added using the --header flag for each header field.
75 `,
76 CobraFunc: func(cmd *cobra.Command) {
77 cmd.Flags().StringP("listen", "l", ":0", "The address to which to bind the listener")
78 cmd.Flags().IntP("status", "s", http.StatusOK, "The response status code")
79 cmd.Flags().StringP("body", "b", "", "The body of the HTTP response")
80 cmd.Flags().BoolP("access-log", "", false, "Enable the access log")
81 cmd.Flags().BoolP("debug", "v", false, "Enable more verbose debug-level logging")
82 cmd.Flags().StringArrayP("header", "H", []string{}, "Set a header on the response (format: \"Field: value\")")
83 cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdRespond)
84 },
85 })
86 }
87
88 // StaticResponse implements a simple responder for static responses.
89 type StaticResponse struct {
90 // The HTTP status code to respond with. Can be an integer or,
91 // if needing to use a placeholder, a string.
92 //
93 // If the status code is 103 (Early Hints), the response headers
94 // will be written to the client immediately, the body will be
95 // ignored, and the next handler will be invoked. This behavior
96 // is EXPERIMENTAL while RFC 8297 is a draft, and may be changed
97 // or removed.
98 StatusCode WeakString `json:"status_code,omitempty"`
99
100 // Header fields to set on the response; overwrites any existing
101 // header fields of the same names after normalization.
102 Headers http.Header `json:"headers,omitempty"`
103
104 // The response body. If non-empty, the Content-Type header may
105 // be added automatically if it is not explicitly configured nor
106 // already set on the response; the default value is
107 // "text/plain; charset=utf-8" unless the body is a valid JSON object
108 // or array, in which case the value will be "application/json".
109 // Other than those common special cases the Content-Type header
110 // should be set explicitly if it is desired because MIME sniffing
111 // is disabled for safety.
112 Body string `json:"body,omitempty"`
113
114 // If true, the server will close the client's connection
115 // after writing the response.
116 Close bool `json:"close,omitempty"`
117
118 // Immediately and forcefully closes the connection without
119 // writing a response. Interrupts any other HTTP streams on
120 // the same connection.
121 Abort bool `json:"abort,omitempty"`
122 }
123
124 // CaddyModule returns the Caddy module information.
125 func (StaticResponse) CaddyModule() caddy.ModuleInfo {
126 return caddy.ModuleInfo{
127 ID: "http.handlers.static_response",
128 New: func() caddy.Module { return new(StaticResponse) },
129 }
130 }
131
132 // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
133 //
134 // respond [<matcher>] <status>|<body> [<status>] {
135 // body <text>
136 // close
137 // }
138 //
139 // If there is just one argument (other than the matcher), it is considered
140 // to be a status code if it's a valid positive integer of 3 digits.
141 func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
142 d.Next() // consume directive name
143 args := d.RemainingArgs()
144 switch len(args) {
145 case 1:
146 if len(args[0]) == 3 {
147 if num, err := strconv.Atoi(args[0]); err == nil && num > 0 {
148 s.StatusCode = WeakString(args[0])
149 break
150 }
151 }
152 s.Body = args[0]
153 case 2:
154 s.Body = args[0]
155 s.StatusCode = WeakString(args[1])
156 default:
157 return d.ArgErr()
158 }
159
160 for d.NextBlock(0) {
161 switch d.Val() {
162 case "body":
163 if s.Body != "" {
164 return d.Err("body already specified")
165 }
166 if !d.AllArgs(&s.Body) {
167 return d.ArgErr()
168 }
169 case "close":
170 if s.Close {
171 return d.Err("close already specified")
172 }
173 s.Close = true
174 default:
175 return d.Errf("unrecognized subdirective '%s'", d.Val())
176 }
177 }
178 return nil
179 }
180
181 func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error {
182 // close the connection immediately
183 if s.Abort {
184 panic(http.ErrAbortHandler)
185 }
186
187 // close the connection after responding
188 if s.Close {
189 r.Close = true
190 w.Header().Set("Connection", "close")
191 }
192
193 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
194
195 // set all headers
196 for field, vals := range s.Headers {
197 field = textproto.CanonicalMIMEHeaderKey(repl.ReplaceKnown(field, ""))
198 newVals := make([]string, len(vals))
199 for i := range vals {
200 newVals[i] = repl.ReplaceKnown(vals[i], "")
201 }
202 w.Header()[field] = newVals
203 }
204
205 // implicitly set Content-Type header if we can do so safely
206 // (this allows templates handler to eval templates successfully
207 // or for clients to render JSON properly which is very common)
208 body := repl.ReplaceKnown(s.Body, "")
209 if body != "" && w.Header().Get("Content-Type") == "" {
210 content := strings.TrimSpace(body)
211 if len(content) > 2 &&
212 (content[0] == '{' && content[len(content)-1] == '}' ||
213 (content[0] == '[' && content[len(content)-1] == ']')) &&
214 json.Valid([]byte(content)) {
215 w.Header().Set("Content-Type", "application/json")
216 } else {
217 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
218 }
219 }
220
221 // do not allow Go to sniff the content-type, for safety
222 if w.Header().Get("Content-Type") == "" {
223 w.Header()["Content-Type"] = nil
224 }
225
226 // get the status code; if this handler exists in an error route,
227 // use the recommended status code as the default; otherwise 200
228 statusCode := http.StatusOK
229 if reqErr, ok := r.Context().Value(ErrorCtxKey).(error); ok {
230 if handlerErr, ok := reqErr.(HandlerError); ok {
231 if handlerErr.StatusCode > 0 {
232 statusCode = handlerErr.StatusCode
233 }
234 }
235 }
236 if codeStr := s.StatusCode.String(); codeStr != "" {
237 intVal, err := strconv.Atoi(repl.ReplaceAll(codeStr, ""))
238 if err != nil {
239 return Error(http.StatusInternalServerError, err)
240 }
241 statusCode = intVal
242 }
243
244 // write headers
245 w.WriteHeader(statusCode)
246
247 // write response body
248 if statusCode != http.StatusEarlyHints && body != "" {
249 fmt.Fprint(w, body) //nolint:gosec // no XSS unless you sabotage your own config
250 }
251
252 // continue handling after Early Hints as they are not the final response
253 if statusCode == http.StatusEarlyHints {
254 return next.ServeHTTP(w, r)
255 }
256
257 return nil
258 }
259
260 func buildHTTPServer(
261 i int,
262 port uint,
263 addr string,
264 statusCode int,
265 hdr http.Header,
266 body string,
267 accessLog bool,
268 ) (*Server, error) {
269 // nolint:prealloc
270 var handlers []json.RawMessage
271
272 // response body supports a basic template; evaluate it
273 tplCtx := struct {
274 N int // server number
275 Port uint // only the port
276 Address string // listener address
277 }{
278 N: i,
279 Port: port,
280 Address: addr,
281 }
282 tpl, err := template.New("body").Parse(body)
283 if err != nil {
284 return nil, err
285 }
286 buf := new(bytes.Buffer)
287 err = tpl.Execute(buf, tplCtx)
288 if err != nil {
289 return nil, err
290 }
291
292 // create route with handler
293 handler := StaticResponse{
294 StatusCode: WeakString(fmt.Sprintf("%d", statusCode)),
295 Headers: hdr,
296 Body: buf.String(),
297 }
298 handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "static_response", nil))
299 route := Route{HandlersRaw: handlers}
300
301 server := &Server{
302 Listen: []string{addr},
303 ReadHeaderTimeout: caddy.Duration(10 * time.Second),
304 IdleTimeout: caddy.Duration(30 * time.Second),
305 MaxHeaderBytes: 1024 * 10,
306 Routes: RouteList{route},
307 AutoHTTPS: &AutoHTTPSConfig{DisableRedir: true},
308 }
309 if accessLog {
310 server.Logs = new(ServerLogConfig)
311 }
312
313 return server, nil
314 }
315
316 func cmdRespond(fl caddycmd.Flags) (int, error) {
317 caddy.TrapSignals()
318
319 // get flag values
320 listen := fl.String("listen")
321 statusCodeFl := fl.Int("status")
322 bodyFl := fl.String("body")
323 accessLog := fl.Bool("access-log")
324 debug := fl.Bool("debug")
325 arg := fl.Arg(0)
326
327 if fl.NArg() > 1 {
328 return caddy.ExitCodeFailedStartup, fmt.Errorf("too many unflagged arguments")
329 }
330
331 // prefer status and body from explicit flags
332 statusCode, body := statusCodeFl, bodyFl
333
334 // figure out if status code was explicitly specified; this lets
335 // us set a non-zero value as the default but is a little hacky
336 statusCodeFlagSpecified := slices.Contains(os.Args, "--status")
337
338 // try to determine what kind of parameter the unnamed argument is
339 if arg != "" {
340 // specifying body and status flags makes the argument redundant/unused
341 if bodyFl != "" && statusCodeFlagSpecified {
342 return caddy.ExitCodeFailedStartup, fmt.Errorf("unflagged argument \"%s\" is overridden by flags", arg)
343 }
344
345 // if a valid 3-digit number, treat as status code; otherwise body
346 if argInt, err := strconv.Atoi(arg); err == nil && !statusCodeFlagSpecified {
347 if argInt >= 100 && argInt <= 999 {
348 statusCode = argInt
349 }
350 } else if body == "" {
351 body = arg
352 }
353 }
354
355 // if we still need a body, see if stdin is being piped
356 if body == "" {
357 stdinInfo, err := os.Stdin.Stat()
358 if err != nil {
359 return caddy.ExitCodeFailedStartup, err
360 }
361 if stdinInfo.Mode()&os.ModeNamedPipe != 0 {
362 bodyBytes, err := io.ReadAll(os.Stdin)
363 if err != nil {
364 return caddy.ExitCodeFailedStartup, err
365 }
366 body = string(bodyBytes)
367 }
368 }
369
370 // build headers map
371 headers, err := fl.GetStringArray("header")
372 if err != nil {
373 return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid header flag: %v", err)
374 }
375 hdr := make(http.Header)
376 for i, h := range headers {
377 key, val, found := strings.Cut(h, ":")
378 key, val = strings.TrimSpace(key), strings.TrimSpace(val)
379 if !found || key == "" || val == "" {
380 return caddy.ExitCodeFailedStartup, fmt.Errorf("header %d: invalid format \"%s\" (expecting \"Field: value\")", i, h)
381 }
382 hdr.Set(key, val)
383 }
384
385 // build each HTTP server
386 httpApp := App{Servers: make(map[string]*Server)}
387
388 // expand listen address, if more than one port
389 listenAddr, err := caddy.ParseNetworkAddress(listen)
390 if err != nil {
391 return caddy.ExitCodeFailedStartup, err
392 }
393
394 if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() {
395 listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
396 for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
397 listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))
398 }
399
400 for i, addr := range listenAddrs {
401 server, err := buildHTTPServer(i, listenAddr.StartPort+uint(i), addr, statusCode, hdr, body, accessLog)
402 if err != nil {
403 return caddy.ExitCodeFailedStartup, err
404 }
405
406 // save server
407 httpApp.Servers[fmt.Sprintf("static%d", i)] = server
408 }
409 } else {
410 server, err := buildHTTPServer(0, 0, listen, statusCode, hdr, body, accessLog)
411 if err != nil {
412 return caddy.ExitCodeFailedStartup, err
413 }
414
415 // save server
416 httpApp.Servers[fmt.Sprintf("static%d", 0)] = server
417 }
418
419 // finish building the config
420 var false bool
421 cfg := &caddy.Config{
422 Admin: &caddy.AdminConfig{
423 Disabled: true,
424 Config: &caddy.ConfigSettings{
425 Persist: &false,
426 },
427 },
428 AppsRaw: caddy.ModuleMap{
429 "http": caddyconfig.JSON(httpApp, nil),
430 },
431 }
432 if debug {
433 cfg.Logging = &caddy.Logging{
434 Logs: map[string]*caddy.CustomLog{
435 "default": {BaseLog: caddy.BaseLog{Level: zap.DebugLevel.CapitalString()}},
436 },
437 }
438 }
439
440 // run it!
441 err = caddy.Run(cfg)
442 if err != nil {
443 return caddy.ExitCodeFailedStartup, err
444 }
445
446 // to print listener addresses, get the active HTTP app
447 loadedHTTPApp, err := caddy.ActiveContext().App("http")
448 if err != nil {
449 return caddy.ExitCodeFailedStartup, err
450 }
451
452 // print each listener address
453 for _, srv := range loadedHTTPApp.(*App).Servers {
454 for _, ln := range srv.listeners {
455 fmt.Printf("Server address: %s\n", ln.Addr())
456 }
457 }
458
459 select {}
460 }
461
462 // Interface guards
463 var (
464 _ MiddlewareHandler = (*StaticResponse)(nil)
465 _ caddyfile.Unmarshaler = (*StaticResponse)(nil)
466 )