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)