mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-22 13:29:01 +08:00
caddyhttp: Implement caddy respond
command (#4870)
This commit is contained in:
parent
ebd6abcbd5
commit
f783290f40
6
admin.go
6
admin.go
|
@ -993,9 +993,9 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
|
|||
id := parts[2]
|
||||
|
||||
// map the ID to the expanded path
|
||||
currentCfgMu.RLock()
|
||||
currentCtxMu.RLock()
|
||||
expanded, ok := rawCfgIndex[id]
|
||||
defer currentCfgMu.RUnlock()
|
||||
defer currentCtxMu.RUnlock()
|
||||
if !ok {
|
||||
return APIError{
|
||||
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
|
||||
// needed. This is a low-level, unsynchronized function; most callers
|
||||
// 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).
|
||||
func unsyncedConfigAccess(method, path string, body []byte, out io.Writer) 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")
|
||||
}
|
||||
|
||||
currentCfgMu.Lock()
|
||||
defer currentCfgMu.Unlock()
|
||||
currentCtxMu.Lock()
|
||||
defer currentCtxMu.Unlock()
|
||||
|
||||
if ifMatchHeader != "" {
|
||||
// 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
|
||||
// and writes its JSON encoding to out.
|
||||
func readConfig(path string, out io.Writer) error {
|
||||
currentCfgMu.RLock()
|
||||
defer currentCfgMu.RUnlock()
|
||||
currentCtxMu.RLock()
|
||||
defer currentCtxMu.RUnlock()
|
||||
return unsyncedConfigAccess(http.MethodGet, path, nil, out)
|
||||
}
|
||||
|
||||
// indexConfigObjects recursively searches ptr for object fields named
|
||||
// "@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
|
||||
// on currentCfgMu.
|
||||
// on currentCtxMu.
|
||||
func indexConfigObjects(ptr interface{}, configPath string, index map[string]string) error {
|
||||
switch val := ptr.(type) {
|
||||
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 does NOT update the raw config state, as this is a
|
||||
// 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,
|
||||
// even if it is configured to.
|
||||
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
|
||||
err = run(newCfg, true)
|
||||
ctx, err := run(newCfg, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// swap old config with the new one
|
||||
oldCfg := currentCfg
|
||||
currentCfg = newCfg
|
||||
// swap old context (including its config) with the new one
|
||||
oldCtx := currentCtx
|
||||
currentCtx = ctx
|
||||
|
||||
// Stop, Cleanup each old app
|
||||
unsyncedStop(oldCfg)
|
||||
unsyncedStop(oldCtx)
|
||||
|
||||
// autosave a non-nil config, if not disabled
|
||||
if allowPersist &&
|
||||
|
@ -373,7 +373,7 @@ func unsyncedDecodeAndRun(cfgJSON []byte, allowPersist bool) error {
|
|||
// This is a low-level function; most callers
|
||||
// will want to use Run instead, which also
|
||||
// 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
|
||||
// modifications if this function errors, we
|
||||
// keep a single error value and scope all
|
||||
|
@ -404,8 +404,8 @@ func run(newCfg *Config, start bool) error {
|
|||
cancel()
|
||||
|
||||
// also undo any other state changes we made
|
||||
if currentCfg != nil {
|
||||
certmagic.Default.Storage = currentCfg.storage
|
||||
if currentCtx.cfg != nil {
|
||||
certmagic.Default.Storage = currentCtx.cfg.storage
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
@ -417,14 +417,14 @@ func run(newCfg *Config, start bool) error {
|
|||
}
|
||||
err = newCfg.Logging.openLogs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// start the admin endpoint (and stop any prior one)
|
||||
if start {
|
||||
err = replaceLocalAdminServer(newCfg)
|
||||
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
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Load and Provision each app and their submodules
|
||||
|
@ -466,18 +466,18 @@ func run(newCfg *Config, start bool) error {
|
|||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
if !start {
|
||||
return nil
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// Provision any admin routers which may need to access
|
||||
// some of the other apps at runtime
|
||||
err = newCfg.Admin.provisionAdminRouters(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// Start
|
||||
|
@ -502,12 +502,12 @@ func run(newCfg *Config, start bool) error {
|
|||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
return ctx, err
|
||||
}
|
||||
|
||||
// now that the user's config is running, finish setting up anything else,
|
||||
// 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.
|
||||
|
@ -612,10 +612,10 @@ type ConfigLoader interface {
|
|||
// stop the others. Stop should only be called
|
||||
// if not replacing with a new config.
|
||||
func Stop() error {
|
||||
currentCfgMu.Lock()
|
||||
defer currentCfgMu.Unlock()
|
||||
unsyncedStop(currentCfg)
|
||||
currentCfg = nil
|
||||
currentCtxMu.Lock()
|
||||
defer currentCtxMu.Unlock()
|
||||
unsyncedStop(currentCtx)
|
||||
currentCtx = Context{}
|
||||
rawCfgJSON = nil
|
||||
rawCfgIndex = nil
|
||||
rawCfg[rawConfigKey] = nil
|
||||
|
@ -628,13 +628,13 @@ func Stop() error {
|
|||
// it is logged and the function continues stopping
|
||||
// the next app. This function assumes all apps in
|
||||
// cfg were successfully started first.
|
||||
func unsyncedStop(cfg *Config) {
|
||||
if cfg == nil {
|
||||
func unsyncedStop(ctx Context) {
|
||||
if ctx.cfg == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// stop each app
|
||||
for name, a := range cfg.apps {
|
||||
for name, a := range ctx.cfg.apps {
|
||||
err := a.Stop()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] stop %s: %v", name, err)
|
||||
|
@ -642,13 +642,13 @@ func unsyncedStop(cfg *Config) {
|
|||
}
|
||||
|
||||
// clean up all modules
|
||||
cfg.cancelFunc()
|
||||
ctx.cfg.cancelFunc()
|
||||
}
|
||||
|
||||
// Validate loads, provisions, and validates
|
||||
// cfg, but does not start running it.
|
||||
func Validate(cfg *Config) error {
|
||||
err := run(cfg, false)
|
||||
_, err := run(cfg, false)
|
||||
if err == nil {
|
||||
cfg.cancelFunc() // call Cleanup on all modules
|
||||
}
|
||||
|
@ -823,16 +823,25 @@ func goModule(mod *debug.Module) *debug.Module {
|
|||
return mod
|
||||
}
|
||||
|
||||
func ActiveContext() Context {
|
||||
currentCtxMu.RLock()
|
||||
defer currentCtxMu.RUnlock()
|
||||
return currentCtx
|
||||
}
|
||||
|
||||
// CtxKey is a value type for use with context.WithValue.
|
||||
type CtxKey string
|
||||
|
||||
// This group of variables pertains to the current configuration.
|
||||
var (
|
||||
// currentCfgMu protects everything in this var block.
|
||||
currentCfgMu sync.RWMutex
|
||||
// currentCtxMu protects everything in this var block.
|
||||
currentCtxMu sync.RWMutex
|
||||
|
||||
// currentCfg is the currently-running configuration.
|
||||
currentCfg *Config
|
||||
// currentCtx is the root context for the currently-running
|
||||
// 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;
|
||||
// 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)
|
||||
fs.SetOutput(buf)
|
||||
buf.Write([]byte("(NOTE: use -- instead of - for flags)\n\n"))
|
||||
fs.PrintDefaults()
|
||||
return buf.String()
|
||||
}
|
||||
|
@ -480,3 +481,16 @@ func CaddyVersion() string {
|
|||
}
|
||||
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
|
||||
go s.Serve(ln)
|
||||
|
||||
srv.listeners = append(srv.listeners, ln)
|
||||
app.servers = append(app.servers, s)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,8 +117,14 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
|
|||
Servers: map[string]*caddyhttp.Server{"static": server},
|
||||
}
|
||||
|
||||
var false bool
|
||||
cfg := &caddy.Config{
|
||||
Admin: &caddy.AdminConfig{Disabled: true},
|
||||
Admin: &caddy.AdminConfig{
|
||||
Disabled: true,
|
||||
Config: &caddy.ConfigSettings{
|
||||
Persist: &false,
|
||||
},
|
||||
},
|
||||
AppsRaw: caddy.ModuleMap{
|
||||
"http": caddyconfig.JSON(httpApp, nil),
|
||||
},
|
||||
|
|
|
@ -172,8 +172,13 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
|
|||
appsRaw["tls"] = caddyconfig.JSON(tlsApp, nil)
|
||||
}
|
||||
|
||||
var false bool
|
||||
cfg := &caddy.Config{
|
||||
Admin: &caddy.AdminConfig{Disabled: true},
|
||||
Admin: &caddy.AdminConfig{Disabled: true,
|
||||
Config: &caddy.ConfigSettings{
|
||||
Persist: &false,
|
||||
},
|
||||
},
|
||||
AppsRaw: appsRaw,
|
||||
}
|
||||
|
||||
|
|
|
@ -132,6 +132,7 @@ type Server struct {
|
|||
primaryHandlerChain Handler
|
||||
errorHandlerChain Handler
|
||||
listenerWrappers []caddy.ListenerWrapper
|
||||
listeners []net.Listener
|
||||
|
||||
tlsApp *caddytls.TLS
|
||||
logger *zap.Logger
|
||||
|
|
|
@ -15,16 +15,71 @@
|
|||
package caddyhttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||
caddycmd "github.com/caddyserver/caddy/v2/cmd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
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.
|
||||
|
@ -165,6 +220,192 @@ func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Hand
|
|||
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
|
||||
var (
|
||||
_ MiddlewareHandler = (*StaticResponse)(nil)
|
||||
|
|
Loading…
Reference in New Issue
Block a user