@marcel91200 / caddy Mirror · read-only / commits / f783290f
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 13, 15:30 UTC

caddyhttp: Implement `caddy respond` command (#4870)

Matt Holt committed Aug 1, 2022 at 13:36 UTC f783290f40febd3eef2a299911ad95bab4d2b414
8 files changed +319 -41
admin.go
+3 -3
@@ -993,9 +993,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
993 id := parts[2]
994
995 // map the ID to the expanded path
996 - currentCfgMu.RLock()
996 + currentCtxMu.RLock()
997 expanded, ok := rawCfgIndex[id]
998 - defer currentCfgMu.RUnlock()
998 + defer currentCtxMu.RUnlock()
999 if !ok {
1000 return APIError{
1001 HTTPStatus: http.StatusNotFound,
@@ -1030,7 +1030,7 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
1030 // the operation at path according to method, using body and out as
1031 // needed. This is a low-level, unsynchronized function; most callers
1032 // will want to use changeConfig or readConfig instead. This requires a
1033 -// read or write lock on currentCfgMu, depending on method (GET needs
1033 +// read or write lock on currentCtxMu, depending on method (GET needs
1034 // only a read lock; all others need a write lock).
1035 func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
1036 var err error
caddy.go
+45 -36
@@ -141,8 +141,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
141 return fmt.Errorf("method not allowed")
142 }
143
144 - currentCfgMu.Lock()
145 - defer currentCfgMu.Unlock()
144 + currentCtxMu.Lock()
145 + defer currentCtxMu.Unlock()
146
147 if ifMatchHeader != "" {
148 // expect the first and last character to be quotes
@@ -242,15 +242,15 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
242 // readConfig traverses the current config to path
243 // and writes its JSON encoding to out.
244 func readConfig(path string, out io.Writer) error {
245 - currentCfgMu.RLock()
246 - defer currentCfgMu.RUnlock()
245 + currentCtxMu.RLock()
246 + defer currentCtxMu.RUnlock()
247 return unsyncedConfigAccess(http.MethodGet, path, nil, out)
248 }
249
250 // indexConfigObjects recursively searches ptr for object fields named
251 // "@id" and maps that ID value to the full configPath in the index.
252 // This function is NOT safe for concurrent access; obtain a write lock
253 -// on currentCfgMu.
253 +// on currentCtxMu.
254 func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
255 switch val := ptr.(type) {
256 case map[string]interface{}:
@@ -290,7 +290,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
290 // it as the new config, replacing any other current config.
291 // It does NOT update the raw config state, as this is a
292 // lower-level function; most callers will want to use Load
293 -// instead. A write lock on currentCfgMu is required! If
293 +// instead. A write lock on currentCtxMu is required! If
294 // allowPersist is false, it will not be persisted to disk,
295 // even if it is configured to.
296 func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
@@ -319,17 +319,17 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
319 }
320
321 // run the new config and start all its apps
322 - err = run(newCfg, true)
322 + ctx, err := run(newCfg, true)
323 if err != nil {
324 return err
325 }
326
327 - // swap old config with the new one
328 - oldCfg := currentCfg
329 - currentCfg = newCfg
327 + // swap old context (including its config) with the new one
328 + oldCtx := currentCtx
329 + currentCtx = ctx
330
331 // Stop, Cleanup each old app
332 - unsyncedStop(oldCfg)
332 + unsyncedStop(oldCtx)
333
334 // autosave a non-nil config, if not disabled
335 if allowPersist &&
@@ -373,7 +373,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
373 // This is a low-level function; most callers
374 // will want to use Run instead, which also
375 // updates the config's raw state.
376 -func run(newCfg *Config, start bool) error {
376 +func run(newCfg *Config, start bool) (Context, error) {
377 // because we will need to roll back any state
378 // modifications if this function errors, we
379 // keep a single error value and scope all
@@ -404,8 +404,8 @@ func run(newCfg *Config, start bool) error {
404 cancel()
405
406 // also undo any other state changes we made
407 - if currentCfg != nil {
408 - certmagic.Default.Storage = currentCfg.storage
407 + if currentCtx.cfg != nil {
408 + certmagic.Default.Storage = currentCtx.cfg.storage
409 }
410 }
411 }()
@@ -417,14 +417,14 @@ func run(newCfg *Config, start bool) error {
417 }
418 err = newCfg.Logging.openLogs(ctx)
419 if err != nil {
420 - return err
420 + return ctx, err
421 }
422
423 // start the admin endpoint (and stop any prior one)
424 if start {
425 err = replaceLocalAdminServer(newCfg)
426 if err != nil {
427 - return fmt.Errorf("starting caddy administration endpoint: %v", err)
427 + return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
428 }
429 }
430
@@ -453,7 +453,7 @@ func run(newCfg *Config, start bool) error {
453 return nil
454 }()
455 if err != nil {
456 - return err
456 + return ctx, err
457 }
458
459 // Load and Provision each app and their submodules
@@ -466,18 +466,18 @@ func run(newCfg *Config, start bool) error {
466 return nil
467 }()
468 if err != nil {
469 - return err
469 + return ctx, err
470 }
471
472 if !start {
473 - return nil
473 + return ctx, nil
474 }
475
476 // Provision any admin routers which may need to access
477 // some of the other apps at runtime
478 err = newCfg.Admin.provisionAdminRouters(ctx)
479 if err != nil {
480 - return err
480 + return ctx, err
481 }
482
483 // Start
@@ -502,12 +502,12 @@ func run(newCfg *Config, start bool) error {
502 return nil
503 }()
504 if err != nil {
505 - return err
505 + return ctx, err
506 }
507
508 // now that the user's config is running, finish setting up anything else,
509 // such as remote admin endpoint, config loader, etc.
510 - return finishSettingUp(ctx, newCfg)
510 + return ctx, finishSettingUp(ctx, newCfg)
511 }
512
513 // finishSettingUp should be run after all apps have successfully started.
@@ -612,10 +612,10 @@ type ConfigLoader interface {
612 // stop the others. Stop should only be called
613 // if not replacing with a new config.
614 func Stop() error {
615 - currentCfgMu.Lock()
616 - defer currentCfgMu.Unlock()
617 - unsyncedStop(currentCfg)
618 - currentCfg = nil
615 + currentCtxMu.Lock()
616 + defer currentCtxMu.Unlock()
617 + unsyncedStop(currentCtx)
618 + currentCtx = Context{}
619 rawCfgJSON = nil
620 rawCfgIndex = nil
621 rawCfg[rawConfigKey] = nil
@@ -628,13 +628,13 @@ func Stop() error {
628 // it is logged and the function continues stopping
629 // the next app. This function assumes all apps in
630 // cfg were successfully started first.
631 -func unsyncedStop(cfg *Config) {
632 - if cfg == nil {
631 +func unsyncedStop(ctx Context) {
632 + if ctx.cfg == nil {
633 return
634 }
635
636 // stop each app
637 - for name, a := range cfg.apps {
637 + for name, a := range ctx.cfg.apps {
638 err := a.Stop()
639 if err != nil {
640 log.Printf("[ERROR] stop %s: %v", name, err)
@@ -642,13 +642,13 @@ func unsyncedStop(cfg *Config) {
642 }
643
644 // clean up all modules
645 - cfg.cancelFunc()
645 + ctx.cfg.cancelFunc()
646 }
647
648 // Validate loads, provisions, and validates
649 // cfg, but does not start running it.
650 func Validate(cfg *Config) error {
651 - err := run(cfg, false)
651 + _, err := run(cfg, false)
652 if err == nil {
653 cfg.cancelFunc() // call Cleanup on all modules
654 }
@@ -823,16 +823,25 @@ func goModule(mod *debug.Module) *debug.Module {
823 return mod
824 }
825
826 +func ActiveContext() Context {
827 + currentCtxMu.RLock()
828 + defer currentCtxMu.RUnlock()
829 + return currentCtx
830 +}
831 +
832 // CtxKey is a value type for use with context.WithValue.
833 type CtxKey string
834
835 // This group of variables pertains to the current configuration.
836 var (
831 - // currentCfgMu protects everything in this var block.
832 - currentCfgMu sync.RWMutex
833 -
834 - // currentCfg is the currently-running configuration.
835 - currentCfg *Config
837 + // currentCtxMu protects everything in this var block.
838 + currentCtxMu sync.RWMutex
839 +
840 + // currentCtx is the root context for the currently-running
841 + // configuration, which can be accessed through this value.
842 + // If the Config contained in this value is not nil, then
843 + // a config is currently active/running.
844 + currentCtx Context
845
846 // rawCfg is the current, generic-decoded configuration;
847 // we initialize it as a map with one field ("config")
cmd/main.go
+14
@@ -338,6 +338,7 @@ func flagHelp(fs *flag.FlagSet) string {
338
339 buf := new(bytes.Buffer)
340 fs.SetOutput(buf)
341 + buf.Write([]byte("(NOTE: use -- instead of - for flags)\n\n"))
342 fs.PrintDefaults()
343 return buf.String()
344 }
@@ -480,3 +481,16 @@ func CaddyVersion() string {
481 }
482 return ver
483 }
484 +
485 +// StringSlice is a flag.Value that enables repeated use of a string flag.
486 +type StringSlice []string
487 +
488 +func (ss StringSlice) String() string { return "[" + strings.Join(ss, ", ") + "]" }
489 +
490 +func (ss *StringSlice) Set(value string) error {
491 + *ss = append(*ss, value)
492 + return nil
493 +}
494 +
495 +// Interface guard
496 +var _ flag.Value = (*StringSlice)(nil)
modules/caddyhttp/app.go
+2
@@ -392,6 +392,8 @@ func (app *App) Start() error {
392
393 //nolint:errcheck
394 go s.Serve(ln)
395 +
396 + srv.listeners = append(srv.listeners, ln)
397 app.servers = append(app.servers, s)
398 }
399 }
modules/caddyhttp/fileserver/command.go
+7 -1
@@ -117,8 +117,14 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
117 Servers: map[string]*caddyhttp.Server{"static": server},
118 }
119
120 + var false bool
121 cfg := &caddy.Config{
121 - Admin: &caddy.AdminConfig{Disabled: true},
122 + Admin: &caddy.AdminConfig{
123 + Disabled: true,
124 + Config: &caddy.ConfigSettings{
125 + Persist: &false,
126 + },
127 + },
128 AppsRaw: caddy.ModuleMap{
129 "http": caddyconfig.JSON(httpApp, nil),
130 },
modules/caddyhttp/reverseproxy/command.go
+6 -1
@@ -172,8 +172,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
172 appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil)
173 }
174
175 + var false bool
176 cfg := &caddy.Config{
176 - Admin: &caddy.AdminConfig{Disabled: true},
177 + Admin: &caddy.AdminConfig{Disabled: true,
178 + Config: &caddy.ConfigSettings{
179 + Persist: &false,
180 + },
181 + },
182 AppsRaw: appsRaw,
183 }
184
modules/caddyhttp/server.go
+1
@@ -132,6 +132,7 @@ type Server struct {
132 primaryHandlerChain Handler
133 errorHandlerChain Handler
134 listenerWrappers []caddy.ListenerWrapper
135 + listeners []net.Listener
136
137 tlsApp *caddytls.TLS
138 logger *zap.Logger
modules/caddyhttp/staticresp.go
+241
@@ -15,16 +15,71 @@
15 package caddyhttp
16
17 import (
18 + "bytes"
19 + "encoding/json"
20 + "flag"
21 "fmt"
22 + "io"
23 "net/http"
24 + "os"
25 "strconv"
26 + "strings"
27 + "text/template"
28 + "time"
29
30 "github.com/caddyserver/caddy/v2"
31 + "github.com/caddyserver/caddy/v2/caddyconfig"
32 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
33 + caddycmd "github.com/caddyserver/caddy/v2/cmd"
34 )
35
36 func init() {
37 caddy.RegisterModule(StaticResponse{})
38 + caddycmd.RegisterCommand(caddycmd.Command{
39 + Name: "respond",
40 + Func: cmdRespond,
41 + Usage: `[--status <code>] [--body <content>] [--listen <addr>] [--access-log] [--debug] [--header "Field: value"] <body|status>`,
42 + Short: "Simple, hard-coded HTTP responses for development and testing",
43 + Long: `
44 +Spins up a quick-and-clean HTTP server for development and testing purposes.
45 +
46 +With no options specified, this command listens on a random available port
47 +and answers HTTP requests with an empty 200 response. The listen address can
48 +be customized with the --listen flag and will always be printed to stdout.
49 +If the listen address includes a port range, multiple servers will be started.
50 +
51 +If a final, unnamed argument is given, it will be treated as a status code
52 +(same as the --status flag) if it is a 3-digit number. Otherwise, it is used
53 +as the response body (same as the --body flag). The --status and --body flags
54 +will always override this argument (for example, to write a body that
55 +literally says "404" but with a status code of 200, do '--status 200 404').
56 +
57 +A body may be given in 3 ways: a flag, a final (and unnamed) argument to
58 +the command, or piped to stdin (if flag and argument are unset). Limited
59 +template evaluation is supported on the body, with the following variables:
60 +
61 + {{.N}} The server number (useful if using a port range)
62 + {{.Port}} The listener port
63 + {{.Address}} The listener address
64 +
65 +(See the docs for the text/template package in the Go standard library for
66 +information about using templates: https://pkg.go.dev/text/template)
67 +
68 +Access/request logging and more verbose debug logging can also be enabled.
69 +
70 +Response headers may be added using the --header flag for each header field.
71 +`,
72 + Flags: func() *flag.FlagSet {
73 + fs := flag.NewFlagSet("respond", flag.ExitOnError)
74 + fs.String("listen", ":0", "The address to which to bind the listener")
75 + fs.Int("status", http.StatusOK, "The response status code")
76 + fs.String("body", "", "The body of the HTTP response")
77 + fs.Bool("access-log", false, "Enable the access log")
78 + fs.Bool("debug", false, "Enable more verbose debug-level logging")
79 + fs.Var(&respondCmdHeaders, "header", "Set a header on the response (format: \"Field: value\"")
80 + return fs
81 + }(),
82 + })
83 }
84
85 // StaticResponse implements a simple responder for static responses.
@@ -165,6 +220,192 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand
220 return nil
221 }
222
223 +func cmdRespond(fl caddycmd.Flags) (int, error) {
224 + caddy.TrapSignals()
225 +
226 + // get flag values
227 + listen := fl.String("listen")
228 + statusCodeFl := fl.Int("status")
229 + bodyFl := fl.String("body")
230 + accessLog := fl.Bool("access-log")
231 + debug := fl.Bool("debug")
232 + arg := fl.Arg(0)
233 +
234 + if fl.NArg() > 1 {
235 + return caddy.ExitCodeFailedStartup, fmt.Errorf("too many unflagged arguments")
236 + }
237 +
238 + // prefer status and body from explicit flags
239 + statusCode, body := statusCodeFl, bodyFl
240 +
241 + // figure out if status code was explicitly specified; this lets
242 + // us set a non-zero value as the default but is a little hacky
243 + var statusCodeFlagSpecified bool
244 + for _, fl := range os.Args {
245 + if fl == "--status" {
246 + statusCodeFlagSpecified = true
247 + break
248 + }
249 + }
250 +
251 + // try to determine what kind of parameter the unnamed argument is
252 + if arg != "" {
253 + // specifying body and status flags makes the argument redundant/unused
254 + if bodyFl != "" && statusCodeFlagSpecified {
255 + return caddy.ExitCodeFailedStartup, fmt.Errorf("unflagged argument \"%s\" is overridden by flags", arg)
256 + }
257 +
258 + // if a valid 3-digit number, treat as status code; otherwise body
259 + if argInt, err := strconv.Atoi(arg); err == nil && !statusCodeFlagSpecified {
260 + if argInt >= 100 && argInt <= 999 {
261 + statusCode = argInt
262 + }
263 + } else if body == "" {
264 + body = arg
265 + }
266 + }
267 +
268 + // if we still need a body, see if stdin is being piped
269 + if body == "" {
270 + stdinInfo, err := os.Stdin.Stat()
271 + if err != nil {
272 + return caddy.ExitCodeFailedStartup, err
273 + }
274 + if stdinInfo.Mode()&os.ModeNamedPipe != 0 {
275 + bodyBytes, err := io.ReadAll(os.Stdin)
276 + if err != nil {
277 + return caddy.ExitCodeFailedStartup, err
278 + }
279 + body = string(bodyBytes)
280 + }
281 + }
282 +
283 + // build headers map
284 + hdr := make(http.Header)
285 + for i, h := range respondCmdHeaders {
286 + key, val, found := cut(h, ":") // TODO: use strings.Cut() once Go 1.18 is our minimum
287 + key, val = strings.TrimSpace(key), strings.TrimSpace(val)
288 + if !found || key == "" || val == "" {
289 + return caddy.ExitCodeFailedStartup, fmt.Errorf("header %d: invalid format \"%s\" (expecting \"Field: value\")", i, h)
290 + }
291 + hdr.Set(key, val)
292 + }
293 +
294 + // expand listen address, if more than one port
295 + listenAddr, err := caddy.ParseNetworkAddress(listen)
296 + if err != nil {
297 + return caddy.ExitCodeFailedStartup, err
298 + }
299 + listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
300 + for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
301 + listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))
302 + }
303 +
304 + // build each HTTP server
305 + httpApp := App{Servers: make(map[string]*Server)}
306 +
307 + for i, addr := range listenAddrs {
308 + var handlers []json.RawMessage
309 +
310 + // response body supports a basic template; evaluate it
311 + tplCtx := struct {
312 + N int // server number
313 + Port uint // only the port
314 + Address string // listener address
315 + }{
316 + N: i,
317 + Port: listenAddr.StartPort + uint(i),
318 + Address: addr,
319 + }
320 + tpl, err := template.New("body").Parse(body)
321 + if err != nil {
322 + return caddy.ExitCodeFailedStartup, err
323 + }
324 + buf := new(bytes.Buffer)
325 + err = tpl.Execute(buf, tplCtx)
326 + if err != nil {
327 + return caddy.ExitCodeFailedStartup, err
328 + }
329 +
330 + // create route with handler
331 + handler := StaticResponse{
332 + StatusCode: WeakString(fmt.Sprintf("%d", statusCode)),
333 + Headers: hdr,
334 + Body: buf.String(),
335 + }
336 + handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "static_response", nil))
337 + route := Route{HandlersRaw: handlers}
338 +
339 + server := &Server{
340 + Listen: []string{addr},
341 + ReadHeaderTimeout: caddy.Duration(10 * time.Second),
342 + IdleTimeout: caddy.Duration(30 * time.Second),
343 + MaxHeaderBytes: 1024 * 10,
344 + Routes: RouteList{route},
345 + AutoHTTPS: &AutoHTTPSConfig{DisableRedir: true},
346 + }
347 + if accessLog {
348 + server.Logs = new(ServerLogConfig)
349 + }
350 +
351 + // save server
352 + httpApp.Servers[fmt.Sprintf("static%d", i)] = server
353 + }
354 +
355 + // finish building the config
356 + var false bool
357 + cfg := &caddy.Config{
358 + Admin: &caddy.AdminConfig{
359 + Disabled: true,
360 + Config: &caddy.ConfigSettings{
361 + Persist: &false,
362 + },
363 + },
364 + AppsRaw: caddy.ModuleMap{
365 + "http": caddyconfig.JSON(httpApp, nil),
366 + },
367 + }
368 + if debug {
369 + cfg.Logging = &caddy.Logging{
370 + Logs: map[string]*caddy.CustomLog{
371 + "default": {Level: "DEBUG"},
372 + },
373 + }
374 + }
375 +
376 + // run it!
377 + err = caddy.Run(cfg)
378 + if err != nil {
379 + return caddy.ExitCodeFailedStartup, err
380 + }
381 +
382 + // to print listener addresses, get the active HTTP app
383 + loadedHTTPApp, err := caddy.ActiveContext().App("http")
384 + if err != nil {
385 + return caddy.ExitCodeFailedStartup, err
386 + }
387 +
388 + // print each listener address
389 + for _, srv := range loadedHTTPApp.(*App).Servers {
390 + for _, ln := range srv.listeners {
391 + fmt.Printf("Server address: %s\n", ln.Addr())
392 + }
393 + }
394 +
395 + select {}
396 +}
397 +
398 +// TODO: delete this and use strings.Cut() once Go 1.18 is our minimum
399 +func cut(s, sep string) (before, after string, found bool) {
400 + if i := strings.Index(s, sep); i >= 0 {
401 + return s[:i], s[i+len(sep):], true
402 + }
403 + return s, "", false
404 +}
405 +
406 +// respondCmdHeaders holds the parsed values from repeated use of the --header flag.
407 +var respondCmdHeaders caddycmd.StringSlice
408 +
409 // Interface guards
410 var (
411 _ MiddlewareHandler = (*StaticResponse)(nil)