mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05:00
caddyhttp: Implement caddy respond
command (#4870)
This commit is contained in:
parent
ebd6abcbd5
commit
f783290f40
8 changed files with 318 additions and 40 deletions
6
admin.go
6
admin.go
|
@ -993,9 +993,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
||||||
id := parts[2]
|
id := parts[2]
|
||||||
|
|
||||||
// map the ID to the expanded path
|
// map the ID to the expanded path
|
||||||
currentCfgMu.RLock()
|
currentCtxMu.RLock()
|
||||||
expanded, ok := rawCfgIndex[id]
|
expanded, ok := rawCfgIndex[id]
|
||||||
defer currentCfgMu.RUnlock()
|
defer currentCtxMu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return APIError{
|
return APIError{
|
||||||
HTTPStatus: http.StatusNotFound,
|
HTTPStatus: http.StatusNotFound,
|
||||||
|
@ -1030,7 +1030,7 @@ func handleStop(w http.ResponseWriter, r *http.Request) error {
|
||||||
// the operation at path according to method, using body and out as
|
// the operation at path according to method, using body and out as
|
||||||
// needed. This is a low-level, unsynchronized function; most callers
|
// needed. This is a low-level, unsynchronized function; most callers
|
||||||
// will want to use changeConfig or readConfig instead. This requires a
|
// will want to use changeConfig or readConfig instead. This requires a
|
||||||
// read or write lock on currentCfgMu, depending on method (GET needs
|
// read or write lock on currentCtxMu, depending on method (GET needs
|
||||||
// only a read lock; all others need a write lock).
|
// only a read lock; all others need a write lock).
|
||||||
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
79
caddy.go
79
caddy.go
|
@ -141,8 +141,8 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
||||||
return fmt.Errorf("method not allowed")
|
return fmt.Errorf("method not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCfgMu.Lock()
|
currentCtxMu.Lock()
|
||||||
defer currentCfgMu.Unlock()
|
defer currentCtxMu.Unlock()
|
||||||
|
|
||||||
if ifMatchHeader != "" {
|
if ifMatchHeader != "" {
|
||||||
// expect the first and last character to be quotes
|
// expect the first and last character to be quotes
|
||||||
|
@ -242,15 +242,15 @@ func changeConfig(method, path string, input []byte, ifMatchHeader string, force
|
||||||
// readConfig traverses the current config to path
|
// readConfig traverses the current config to path
|
||||||
// and writes its JSON encoding to out.
|
// and writes its JSON encoding to out.
|
||||||
func readConfig(path string, out io.Writer) error {
|
func readConfig(path string, out io.Writer) error {
|
||||||
currentCfgMu.RLock()
|
currentCtxMu.RLock()
|
||||||
defer currentCfgMu.RUnlock()
|
defer currentCtxMu.RUnlock()
|
||||||
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexConfigObjects recursively searches ptr for object fields named
|
// indexConfigObjects recursively searches ptr for object fields named
|
||||||
// "@id" and maps that ID value to the full configPath in the index.
|
// "@id" and maps that ID value to the full configPath in the index.
|
||||||
// This function is NOT safe for concurrent access; obtain a write lock
|
// This function is NOT safe for concurrent access; obtain a write lock
|
||||||
// on currentCfgMu.
|
// on currentCtxMu.
|
||||||
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
|
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
|
||||||
switch val := ptr.(type) {
|
switch val := ptr.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
|
@ -290,7 +290,7 @@ func indexConfigObjects(ptr interface{}, configPath string, index map[string]str
|
||||||
// it as the new config, replacing any other current config.
|
// it as the new config, replacing any other current config.
|
||||||
// It does NOT update the raw config state, as this is a
|
// It does NOT update the raw config state, as this is a
|
||||||
// lower-level function; most callers will want to use Load
|
// lower-level function; most callers will want to use Load
|
||||||
// instead. A write lock on currentCfgMu is required! If
|
// instead. A write lock on currentCtxMu is required! If
|
||||||
// allowPersist is false, it will not be persisted to disk,
|
// allowPersist is false, it will not be persisted to disk,
|
||||||
// even if it is configured to.
|
// even if it is configured to.
|
||||||
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||||
|
@ -319,17 +319,17 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the new config and start all its apps
|
// run the new config and start all its apps
|
||||||
err = run(newCfg, true)
|
ctx, err := run(newCfg, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// swap old config with the new one
|
// swap old context (including its config) with the new one
|
||||||
oldCfg := currentCfg
|
oldCtx := currentCtx
|
||||||
currentCfg = newCfg
|
currentCtx = ctx
|
||||||
|
|
||||||
// Stop, Cleanup each old app
|
// Stop, Cleanup each old app
|
||||||
unsyncedStop(oldCfg)
|
unsyncedStop(oldCtx)
|
||||||
|
|
||||||
// autosave a non-nil config, if not disabled
|
// autosave a non-nil config, if not disabled
|
||||||
if allowPersist &&
|
if allowPersist &&
|
||||||
|
@ -373,7 +373,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
||||||
// This is a low-level function; most callers
|
// This is a low-level function; most callers
|
||||||
// will want to use Run instead, which also
|
// will want to use Run instead, which also
|
||||||
// updates the config's raw state.
|
// updates the config's raw state.
|
||||||
func run(newCfg *Config, start bool) error {
|
func run(newCfg *Config, start bool) (Context, error) {
|
||||||
// because we will need to roll back any state
|
// because we will need to roll back any state
|
||||||
// modifications if this function errors, we
|
// modifications if this function errors, we
|
||||||
// keep a single error value and scope all
|
// keep a single error value and scope all
|
||||||
|
@ -404,8 +404,8 @@ func run(newCfg *Config, start bool) error {
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
// also undo any other state changes we made
|
// also undo any other state changes we made
|
||||||
if currentCfg != nil {
|
if currentCtx.cfg != nil {
|
||||||
certmagic.Default.Storage = currentCfg.storage
|
certmagic.Default.Storage = currentCtx.cfg.storage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -417,14 +417,14 @@ func run(newCfg *Config, start bool) error {
|
||||||
}
|
}
|
||||||
err = newCfg.Logging.openLogs(ctx)
|
err = newCfg.Logging.openLogs(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the admin endpoint (and stop any prior one)
|
// start the admin endpoint (and stop any prior one)
|
||||||
if start {
|
if start {
|
||||||
err = replaceLocalAdminServer(newCfg)
|
err = replaceLocalAdminServer(newCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("starting caddy administration endpoint: %v", err)
|
return ctx, fmt.Errorf("starting caddy administration endpoint: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +453,7 @@ func run(newCfg *Config, start bool) error {
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load and Provision each app and their submodules
|
// Load and Provision each app and their submodules
|
||||||
|
@ -466,18 +466,18 @@ func run(newCfg *Config, start bool) error {
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !start {
|
if !start {
|
||||||
return nil
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provision any admin routers which may need to access
|
// Provision any admin routers which may need to access
|
||||||
// some of the other apps at runtime
|
// some of the other apps at runtime
|
||||||
err = newCfg.Admin.provisionAdminRouters(ctx)
|
err = newCfg.Admin.provisionAdminRouters(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
|
@ -502,12 +502,12 @@ func run(newCfg *Config, start bool) error {
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return ctx, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// now that the user's config is running, finish setting up anything else,
|
// now that the user's config is running, finish setting up anything else,
|
||||||
// such as remote admin endpoint, config loader, etc.
|
// such as remote admin endpoint, config loader, etc.
|
||||||
return finishSettingUp(ctx, newCfg)
|
return ctx, finishSettingUp(ctx, newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// finishSettingUp should be run after all apps have successfully started.
|
// finishSettingUp should be run after all apps have successfully started.
|
||||||
|
@ -612,10 +612,10 @@ type ConfigLoader interface {
|
||||||
// stop the others. Stop should only be called
|
// stop the others. Stop should only be called
|
||||||
// if not replacing with a new config.
|
// if not replacing with a new config.
|
||||||
func Stop() error {
|
func Stop() error {
|
||||||
currentCfgMu.Lock()
|
currentCtxMu.Lock()
|
||||||
defer currentCfgMu.Unlock()
|
defer currentCtxMu.Unlock()
|
||||||
unsyncedStop(currentCfg)
|
unsyncedStop(currentCtx)
|
||||||
currentCfg = nil
|
currentCtx = Context{}
|
||||||
rawCfgJSON = nil
|
rawCfgJSON = nil
|
||||||
rawCfgIndex = nil
|
rawCfgIndex = nil
|
||||||
rawCfg[rawConfigKey] = nil
|
rawCfg[rawConfigKey] = nil
|
||||||
|
@ -628,13 +628,13 @@ func Stop() error {
|
||||||
// it is logged and the function continues stopping
|
// it is logged and the function continues stopping
|
||||||
// the next app. This function assumes all apps in
|
// the next app. This function assumes all apps in
|
||||||
// cfg were successfully started first.
|
// cfg were successfully started first.
|
||||||
func unsyncedStop(cfg *Config) {
|
func unsyncedStop(ctx Context) {
|
||||||
if cfg == nil {
|
if ctx.cfg == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop each app
|
// stop each app
|
||||||
for name, a := range cfg.apps {
|
for name, a := range ctx.cfg.apps {
|
||||||
err := a.Stop()
|
err := a.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] stop %s: %v", name, err)
|
log.Printf("[ERROR] stop %s: %v", name, err)
|
||||||
|
@ -642,13 +642,13 @@ func unsyncedStop(cfg *Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up all modules
|
// clean up all modules
|
||||||
cfg.cancelFunc()
|
ctx.cfg.cancelFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate loads, provisions, and validates
|
// Validate loads, provisions, and validates
|
||||||
// cfg, but does not start running it.
|
// cfg, but does not start running it.
|
||||||
func Validate(cfg *Config) error {
|
func Validate(cfg *Config) error {
|
||||||
err := run(cfg, false)
|
_, err := run(cfg, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
cfg.cancelFunc() // call Cleanup on all modules
|
cfg.cancelFunc() // call Cleanup on all modules
|
||||||
}
|
}
|
||||||
|
@ -823,16 +823,25 @@ func goModule(mod *debug.Module) *debug.Module {
|
||||||
return mod
|
return mod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ActiveContext() Context {
|
||||||
|
currentCtxMu.RLock()
|
||||||
|
defer currentCtxMu.RUnlock()
|
||||||
|
return currentCtx
|
||||||
|
}
|
||||||
|
|
||||||
// CtxKey is a value type for use with context.WithValue.
|
// CtxKey is a value type for use with context.WithValue.
|
||||||
type CtxKey string
|
type CtxKey string
|
||||||
|
|
||||||
// This group of variables pertains to the current configuration.
|
// This group of variables pertains to the current configuration.
|
||||||
var (
|
var (
|
||||||
// currentCfgMu protects everything in this var block.
|
// currentCtxMu protects everything in this var block.
|
||||||
currentCfgMu sync.RWMutex
|
currentCtxMu sync.RWMutex
|
||||||
|
|
||||||
// currentCfg is the currently-running configuration.
|
// currentCtx is the root context for the currently-running
|
||||||
currentCfg *Config
|
// configuration, which can be accessed through this value.
|
||||||
|
// If the Config contained in this value is not nil, then
|
||||||
|
// a config is currently active/running.
|
||||||
|
currentCtx Context
|
||||||
|
|
||||||
// rawCfg is the current, generic-decoded configuration;
|
// rawCfg is the current, generic-decoded configuration;
|
||||||
// we initialize it as a map with one field ("config")
|
// we initialize it as a map with one field ("config")
|
||||||
|
|
14
cmd/main.go
14
cmd/main.go
|
@ -338,6 +338,7 @@ func flagHelp(fs *flag.FlagSet) string {
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
fs.SetOutput(buf)
|
fs.SetOutput(buf)
|
||||||
|
buf.Write([]byte("(NOTE: use -- instead of - for flags)\n\n"))
|
||||||
fs.PrintDefaults()
|
fs.PrintDefaults()
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
@ -480,3 +481,16 @@ func CaddyVersion() string {
|
||||||
}
|
}
|
||||||
return ver
|
return ver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StringSlice is a flag.Value that enables repeated use of a string flag.
|
||||||
|
type StringSlice []string
|
||||||
|
|
||||||
|
func (ss StringSlice) String() string { return "[" + strings.Join(ss, ", ") + "]" }
|
||||||
|
|
||||||
|
func (ss *StringSlice) Set(value string) error {
|
||||||
|
*ss = append(*ss, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guard
|
||||||
|
var _ flag.Value = (*StringSlice)(nil)
|
||||||
|
|
|
@ -392,6 +392,8 @@ func (app *App) Start() error {
|
||||||
|
|
||||||
//nolint:errcheck
|
//nolint:errcheck
|
||||||
go s.Serve(ln)
|
go s.Serve(ln)
|
||||||
|
|
||||||
|
srv.listeners = append(srv.listeners, ln)
|
||||||
app.servers = append(app.servers, s)
|
app.servers = append(app.servers, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,8 +117,14 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
||||||
Servers: map[string]*caddyhttp.Server{"static": server},
|
Servers: map[string]*caddyhttp.Server{"static": server},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var false bool
|
||||||
cfg := &caddy.Config{
|
cfg := &caddy.Config{
|
||||||
Admin: &caddy.AdminConfig{Disabled: true},
|
Admin: &caddy.AdminConfig{
|
||||||
|
Disabled: true,
|
||||||
|
Config: &caddy.ConfigSettings{
|
||||||
|
Persist: &false,
|
||||||
|
},
|
||||||
|
},
|
||||||
AppsRaw: caddy.ModuleMap{
|
AppsRaw: caddy.ModuleMap{
|
||||||
"http": caddyconfig.JSON(httpApp, nil),
|
"http": caddyconfig.JSON(httpApp, nil),
|
||||||
},
|
},
|
||||||
|
|
|
@ -172,8 +172,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
||||||
appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil)
|
appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var false bool
|
||||||
cfg := &caddy.Config{
|
cfg := &caddy.Config{
|
||||||
Admin: &caddy.AdminConfig{Disabled: true},
|
Admin: &caddy.AdminConfig{Disabled: true,
|
||||||
|
Config: &caddy.ConfigSettings{
|
||||||
|
Persist: &false,
|
||||||
|
},
|
||||||
|
},
|
||||||
AppsRaw: appsRaw,
|
AppsRaw: appsRaw,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -132,6 +132,7 @@ type Server struct {
|
||||||
primaryHandlerChain Handler
|
primaryHandlerChain Handler
|
||||||
errorHandlerChain Handler
|
errorHandlerChain Handler
|
||||||
listenerWrappers []caddy.ListenerWrapper
|
listenerWrappers []caddy.ListenerWrapper
|
||||||
|
listeners []net.Listener
|
||||||
|
|
||||||
tlsApp *caddytls.TLS
|
tlsApp *caddytls.TLS
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
|
|
@ -15,16 +15,71 @@
|
||||||
package caddyhttp
|
package caddyhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(StaticResponse{})
|
caddy.RegisterModule(StaticResponse{})
|
||||||
|
caddycmd.RegisterCommand(caddycmd.Command{
|
||||||
|
Name: "respond",
|
||||||
|
Func: cmdRespond,
|
||||||
|
Usage: `[--status <code>] [--body <content>] [--listen <addr>] [--access-log] [--debug] [--header "Field: value"] <body|status>`,
|
||||||
|
Short: "Simple, hard-coded HTTP responses for development and testing",
|
||||||
|
Long: `
|
||||||
|
Spins up a quick-and-clean HTTP server for development and testing purposes.
|
||||||
|
|
||||||
|
With no options specified, this command listens on a random available port
|
||||||
|
and answers HTTP requests with an empty 200 response. The listen address can
|
||||||
|
be customized with the --listen flag and will always be printed to stdout.
|
||||||
|
If the listen address includes a port range, multiple servers will be started.
|
||||||
|
|
||||||
|
If a final, unnamed argument is given, it will be treated as a status code
|
||||||
|
(same as the --status flag) if it is a 3-digit number. Otherwise, it is used
|
||||||
|
as the response body (same as the --body flag). The --status and --body flags
|
||||||
|
will always override this argument (for example, to write a body that
|
||||||
|
literally says "404" but with a status code of 200, do '--status 200 404').
|
||||||
|
|
||||||
|
A body may be given in 3 ways: a flag, a final (and unnamed) argument to
|
||||||
|
the command, or piped to stdin (if flag and argument are unset). Limited
|
||||||
|
template evaluation is supported on the body, with the following variables:
|
||||||
|
|
||||||
|
{{.N}} The server number (useful if using a port range)
|
||||||
|
{{.Port}} The listener port
|
||||||
|
{{.Address}} The listener address
|
||||||
|
|
||||||
|
(See the docs for the text/template package in the Go standard library for
|
||||||
|
information about using templates: https://pkg.go.dev/text/template)
|
||||||
|
|
||||||
|
Access/request logging and more verbose debug logging can also be enabled.
|
||||||
|
|
||||||
|
Response headers may be added using the --header flag for each header field.
|
||||||
|
`,
|
||||||
|
Flags: func() *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet("respond", flag.ExitOnError)
|
||||||
|
fs.String("listen", ":0", "The address to which to bind the listener")
|
||||||
|
fs.Int("status", http.StatusOK, "The response status code")
|
||||||
|
fs.String("body", "", "The body of the HTTP response")
|
||||||
|
fs.Bool("access-log", false, "Enable the access log")
|
||||||
|
fs.Bool("debug", false, "Enable more verbose debug-level logging")
|
||||||
|
fs.Var(&respondCmdHeaders, "header", "Set a header on the response (format: \"Field: value\"")
|
||||||
|
return fs
|
||||||
|
}(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// StaticResponse implements a simple responder for static responses.
|
// StaticResponse implements a simple responder for static responses.
|
||||||
|
@ -165,6 +220,192 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cmdRespond(fl caddycmd.Flags) (int, error) {
|
||||||
|
caddy.TrapSignals()
|
||||||
|
|
||||||
|
// get flag values
|
||||||
|
listen := fl.String("listen")
|
||||||
|
statusCodeFl := fl.Int("status")
|
||||||
|
bodyFl := fl.String("body")
|
||||||
|
accessLog := fl.Bool("access-log")
|
||||||
|
debug := fl.Bool("debug")
|
||||||
|
arg := fl.Arg(0)
|
||||||
|
|
||||||
|
if fl.NArg() > 1 {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("too many unflagged arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
// prefer status and body from explicit flags
|
||||||
|
statusCode, body := statusCodeFl, bodyFl
|
||||||
|
|
||||||
|
// figure out if status code was explicitly specified; this lets
|
||||||
|
// us set a non-zero value as the default but is a little hacky
|
||||||
|
var statusCodeFlagSpecified bool
|
||||||
|
for _, fl := range os.Args {
|
||||||
|
if fl == "--status" {
|
||||||
|
statusCodeFlagSpecified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to determine what kind of parameter the unnamed argument is
|
||||||
|
if arg != "" {
|
||||||
|
// specifying body and status flags makes the argument redundant/unused
|
||||||
|
if bodyFl != "" && statusCodeFlagSpecified {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("unflagged argument \"%s\" is overridden by flags", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if a valid 3-digit number, treat as status code; otherwise body
|
||||||
|
if argInt, err := strconv.Atoi(arg); err == nil && !statusCodeFlagSpecified {
|
||||||
|
if argInt >= 100 && argInt <= 999 {
|
||||||
|
statusCode = argInt
|
||||||
|
}
|
||||||
|
} else if body == "" {
|
||||||
|
body = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we still need a body, see if stdin is being piped
|
||||||
|
if body == "" {
|
||||||
|
stdinInfo, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
if stdinInfo.Mode()&os.ModeNamedPipe != 0 {
|
||||||
|
bodyBytes, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
body = string(bodyBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build headers map
|
||||||
|
hdr := make(http.Header)
|
||||||
|
for i, h := range respondCmdHeaders {
|
||||||
|
key, val, found := cut(h, ":") // TODO: use strings.Cut() once Go 1.18 is our minimum
|
||||||
|
key, val = strings.TrimSpace(key), strings.TrimSpace(val)
|
||||||
|
if !found || key == "" || val == "" {
|
||||||
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("header %d: invalid format \"%s\" (expecting \"Field: value\")", i, h)
|
||||||
|
}
|
||||||
|
hdr.Set(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expand listen address, if more than one port
|
||||||
|
listenAddr, err := caddy.ParseNetworkAddress(listen)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
|
||||||
|
for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
|
||||||
|
listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
// build each HTTP server
|
||||||
|
httpApp := App{Servers: make(map[string]*Server)}
|
||||||
|
|
||||||
|
for i, addr := range listenAddrs {
|
||||||
|
var handlers []json.RawMessage
|
||||||
|
|
||||||
|
// response body supports a basic template; evaluate it
|
||||||
|
tplCtx := struct {
|
||||||
|
N int // server number
|
||||||
|
Port uint // only the port
|
||||||
|
Address string // listener address
|
||||||
|
}{
|
||||||
|
N: i,
|
||||||
|
Port: listenAddr.StartPort + uint(i),
|
||||||
|
Address: addr,
|
||||||
|
}
|
||||||
|
tpl, err := template.New("body").Parse(body)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err = tpl.Execute(buf, tplCtx)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create route with handler
|
||||||
|
handler := StaticResponse{
|
||||||
|
StatusCode: WeakString(fmt.Sprintf("%d", statusCode)),
|
||||||
|
Headers: hdr,
|
||||||
|
Body: buf.String(),
|
||||||
|
}
|
||||||
|
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "static_response", nil))
|
||||||
|
route := Route{HandlersRaw: handlers}
|
||||||
|
|
||||||
|
server := &Server{
|
||||||
|
Listen: []string{addr},
|
||||||
|
ReadHeaderTimeout: caddy.Duration(10 * time.Second),
|
||||||
|
IdleTimeout: caddy.Duration(30 * time.Second),
|
||||||
|
MaxHeaderBytes: 1024 * 10,
|
||||||
|
Routes: RouteList{route},
|
||||||
|
AutoHTTPS: &AutoHTTPSConfig{DisableRedir: true},
|
||||||
|
}
|
||||||
|
if accessLog {
|
||||||
|
server.Logs = new(ServerLogConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save server
|
||||||
|
httpApp.Servers[fmt.Sprintf("static%d", i)] = server
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish building the config
|
||||||
|
var false bool
|
||||||
|
cfg := &caddy.Config{
|
||||||
|
Admin: &caddy.AdminConfig{
|
||||||
|
Disabled: true,
|
||||||
|
Config: &caddy.ConfigSettings{
|
||||||
|
Persist: &false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppsRaw: caddy.ModuleMap{
|
||||||
|
"http": caddyconfig.JSON(httpApp, nil),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
cfg.Logging = &caddy.Logging{
|
||||||
|
Logs: map[string]*caddy.CustomLog{
|
||||||
|
"default": {Level: "DEBUG"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run it!
|
||||||
|
err = caddy.Run(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// to print listener addresses, get the active HTTP app
|
||||||
|
loadedHTTPApp, err := caddy.ActiveContext().App("http")
|
||||||
|
if err != nil {
|
||||||
|
return caddy.ExitCodeFailedStartup, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// print each listener address
|
||||||
|
for _, srv := range loadedHTTPApp.(*App).Servers {
|
||||||
|
for _, ln := range srv.listeners {
|
||||||
|
fmt.Printf("Server address: %s\n", ln.Addr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: delete this and use strings.Cut() once Go 1.18 is our minimum
|
||||||
|
func cut(s, sep string) (before, after string, found bool) {
|
||||||
|
if i := strings.Index(s, sep); i >= 0 {
|
||||||
|
return s[:i], s[i+len(sep):], true
|
||||||
|
}
|
||||||
|
return s, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// respondCmdHeaders holds the parsed values from repeated use of the --header flag.
|
||||||
|
var respondCmdHeaders caddycmd.StringSlice
|
||||||
|
|
||||||
// Interface guards
|
// Interface guards
|
||||||
var (
|
var (
|
||||||
_ MiddlewareHandler = (*StaticResponse)(nil)
|
_ MiddlewareHandler = (*StaticResponse)(nil)
|
||||||
|
|
Loading…
Reference in a new issue