diff options
Diffstat (limited to 'cmd/commands.go')
| -rw-r--r-- | cmd/commands.go | 497 | 
1 files changed, 179 insertions, 318 deletions
| diff --git a/cmd/commands.go b/cmd/commands.go index 2526e45..8a9d482 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -15,343 +15,204 @@  package caddycmd  import ( -	"bytes" -	"crypto/rand" -	"encoding/json"  	"flag" -	"fmt" -	"io" -	"io/ioutil" -	"log" -	"net" -	"net/http" -	"os" -	"os/exec" - -	"strings" - -	"github.com/caddyserver/caddy/v2" -	"github.com/caddyserver/caddy/v2/caddyconfig" -	"github.com/keybase/go-ps" -	"github.com/mholt/certmagic" +	"regexp"  ) -func cmdStart() (int, error) { -	startCmd := flag.NewFlagSet("start", flag.ExitOnError) -	startCmdConfigFlag := startCmd.String("config", "", "Configuration file") -	startCmdConfigAdapterFlag := startCmd.String("config-adapter", "", "Name of config adapter to apply") -	startCmd.Parse(os.Args[2:]) +// Command represents a subcommand. All fields +// are required to be set except for Flags if +// there are no flags and Usage if there are +// no flags or arguments. +type Command struct { +	Name string -	// open a listener to which the child process will connect when -	// it is ready to confirm that it has successfully started -	ln, err := net.Listen("tcp", "127.0.0.1:0") -	if err != nil { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("opening listener for success confirmation: %v", err) -	} -	defer ln.Close() +	// Run is a function that executes a subcommand. +	// It returns an exit code and any associated error. +	// Takes non-flag commandline arguments as args. +	// Flag must be parsed before Run is executed. +	Func CommandFunc -	// craft the command with a pingback address and with a -	// pipe for its stdin, so we can tell it our confirmation -	// code that we expect so that some random port scan at -	// the most unfortunate time won't fool us into thinking -	// the child succeeded (i.e. the alternative is to just -	// wait for any connection on our listener, but better to -	// ensure it's the process we're expecting - we can be -	// sure by giving it some random bytes and having it echo -	// them back to us) -	cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) -	if *startCmdConfigFlag != "" { -		cmd.Args = append(cmd.Args, "--config", *startCmdConfigFlag) -	} -	if *startCmdConfigAdapterFlag != "" { -		cmd.Args = append(cmd.Args, "--config-adapter", *startCmdConfigAdapterFlag) -	} -	stdinpipe, err := cmd.StdinPipe() -	if err != nil { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("creating stdin pipe: %v", err) -	} -	cmd.Stdout = os.Stdout -	cmd.Stderr = os.Stderr +	// Usage is the one-line message explaining args, flags. +	Usage string -	// generate the random bytes we'll send to the child process -	expect := make([]byte, 32) -	_, err = rand.Read(expect) -	if err != nil { -		return caddy.ExitCodeFailedStartup, fmt.Errorf("generating random confirmation bytes: %v", err) -	} +	// Short is the short description for command. +	Short string -	// begin writing the confirmation bytes to the child's -	// stdin; use a goroutine since the child hasn't been -	// started yet, and writing sychronously would result -	// in a deadlock -	go func() { -		stdinpipe.Write(expect) -		stdinpipe.Close() -	}() +	// Long is the message for 'caddy help <command>' +	Long string -	// start the process -	err = cmd.Start() -	if err != nil { -		return caddy.ExitCodeFailedStartup, fmt.Errorf("starting caddy process: %v", err) -	} - -	// there are two ways we know we're done: either -	// the process will connect to our listener, or -	// it will exit with an error -	success, exit := make(chan struct{}), make(chan error) - -	// in one goroutine, we await the success of the child process -	go func() { -		for { -			conn, err := ln.Accept() -			if err != nil { -				if !strings.Contains(err.Error(), "use of closed network connection") { -					log.Println(err) -				} -				break -			} -			err = handlePingbackConn(conn, expect) -			if err == nil { -				close(success) -				break -			} -			log.Println(err) -		} -	}() - -	// in another goroutine, we await the failure of the child process -	go func() { -		err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks -		exit <- err       // sending on separate line ensures select won't trigger until after Wait unblocks -	}() - -	// when one of the goroutines unblocks, we're done and can exit -	select { -	case <-success: -		fmt.Printf("Successfully started Caddy (pid=%d)\n", cmd.Process.Pid) -	case err := <-exit: -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("caddy process exited with error: %v", err) -	} - -	return caddy.ExitCodeSuccess, nil +	// Flags is flagset for command. +	Flags *flag.FlagSet  } -func cmdRun() (int, error) { -	runCmd := flag.NewFlagSet("run", flag.ExitOnError) -	runCmdConfigFlag := runCmd.String("config", "", "Configuration file") -	runCmdConfigAdapterFlag := runCmd.String("config-adapter", "", "Name of config adapter to apply") -	runCmdPrintEnvFlag := runCmd.Bool("print-env", false, "Print environment") -	runCmdPingbackFlag := runCmd.String("pingback", "", "Echo confirmation bytes to this address on success") -	runCmd.Parse(os.Args[2:]) - -	// if we are supposed to print the environment, do that first -	if *runCmdPrintEnvFlag { -		exitCode, err := cmdEnviron() -		if err != nil { -			return exitCode, err -		} -	} - -	// get the config in caddy's native format -	config, err := loadConfig(*runCmdConfigFlag, *runCmdConfigAdapterFlag) -	if err != nil { -		return caddy.ExitCodeFailedStartup, err -	} - -	// set a fitting User-Agent for ACME requests -	goModule := caddy.GoModule() -	cleanModVersion := strings.TrimPrefix(goModule.Version, "v") -	certmagic.UserAgent = "Caddy/" + cleanModVersion - -	// start the admin endpoint along with any initial config -	err = caddy.StartAdmin(config) -	if err != nil { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("starting caddy administration endpoint: %v", err) -	} -	defer caddy.StopAdmin() - -	// if we are to report to another process the successful start -	// of the server, do so now by echoing back contents of stdin -	if *runCmdPingbackFlag != "" { -		confirmationBytes, err := ioutil.ReadAll(os.Stdin) -		if err != nil { -			return caddy.ExitCodeFailedStartup, -				fmt.Errorf("reading confirmation bytes from stdin: %v", err) -		} -		conn, err := net.Dial("tcp", *runCmdPingbackFlag) -		if err != nil { -			return caddy.ExitCodeFailedStartup, -				fmt.Errorf("dialing confirmation address: %v", err) -		} -		defer conn.Close() -		_, err = conn.Write(confirmationBytes) -		if err != nil { -			return caddy.ExitCodeFailedStartup, -				fmt.Errorf("writing confirmation bytes to %s: %v", *runCmdPingbackFlag, err) -		} -	} - -	select {} +// CommandFunc is a command's function. It runs the +// command and returns the proper exit code along with +// any error that occurred. +type CommandFunc func(Flags) (int, error) + +var commands = map[string]Command{ +	"start": { +		Name:  "start", +		Func:  cmdStart, +		Usage: "[--config <path>] [--config-adapter <name>]", +		Short: "Starts the Caddy process and returns after server has started.", +		Long: ` +Starts the Caddy process, optionally bootstrapped with an initial +config file. Blocks until server is successfully running (or fails to run), +then returns. On Windows, the child process will remain attached to the +terminal, so closing the window will forcefully stop Caddy. See run for more +details.`, +		Flags: func() *flag.FlagSet { +			fs := flag.NewFlagSet("start", flag.ExitOnError) +			fs.String("config", "", "Configuration file") +			fs.String("config-adapter", "", "Name of config adapter to apply") +			return fs +		}(), +	}, + +	"run": { +		Name:  "run", +		Func:  cmdRun, +		Usage: "[--config <path>] [--config-adapter <name>] [--print-env]", +		Short: `Starts the Caddy process and blocks indefinitely.`, +		Long: ` +Same as start, but blocks indefinitely; i.e. runs Caddy in "daemon" mode. On +Windows, this is recommended over caddy start when running Caddy manually since +it will be more obvious that Caddy is still running and bound to the terminal +window. + +If a config file is specified, it will be applied immediately after the process +is running. If the config file is not in Caddy's native JSON format, you can +specify an adapter with --config-adapter to adapt the given config file to +Caddy's native format. The config adapter must be a registered module. Any +warnings will be printed to the log, but beware that any adaptation without +errors will immediately be used. If you want to review the results of the +adaptation first, use adapt-config. + +As a special case, if the current working directory has a file called +"Caddyfile" and the caddyfile config adapter is plugged in (default), then that +file will be loaded and used to configure Caddy, even without any command line +flags. + +If --print-env is specified, the environment as seen by the Caddy process will +be printed before starting. This is the same as the environ command but does +not quit after printing.`, +		Flags: func() *flag.FlagSet { +			fs := flag.NewFlagSet("run", flag.ExitOnError) +			fs.String("config", "", "Configuration file") +			fs.String("config-adapter", "", "Name of config adapter to apply") +			fs.Bool("print-env", false, "Print environment") +			fs.String("pingback", "", "Echo confirmation bytes to this address on success") +			return fs +		}(), +	}, + +	"stop": { +		Name:  "stop", +		Func:  cmdStop, +		Short: "Gracefully stops the running Caddy process", +		Long: `Gracefully stops the running Caddy process. (Note: this will stop any process +named the same as the executable.) On Windows, this stop is forceful and Caddy +will not have an opportunity to clean up any active locks; for a graceful +shutdown on Windows, use Ctrl+C or the /stop endpoint.`, +	}, + +	"reload": { +		Name:  "reload", +		Func:  cmdReload, +		Usage: "--config <path> [--config-adapter <name>] [--address <interface>]", +		Short: "Gives the running Caddy instance a new configuration", +		Long: `Gives the running Caddy instance a new configuration. This has the same effect +as POSTing a document to the /load endpoint, but is convenient for simple +workflows revolving around config files. Since the admin endpoint is +configurable, the endpoint configuration is loaded from the --address flag if +specified; otherwise it is loaded from the given config file; otherwise the +default is assumed.`, +		Flags: func() *flag.FlagSet { +			fs := flag.NewFlagSet("load", flag.ExitOnError) +			fs.String("config", "", "Configuration file") +			fs.String("config-adapter", "", "Name of config adapter to apply") +			fs.String("address", "", "Address of the administration listener, if different from config") +			return fs +		}(), +	}, + +	"version": { +		Name:  "version", +		Func:  cmdVersion, +		Short: "Prints the version.", +		Long:  `Prints the version.`, +	}, + +	"list-modules": { +		Name:  "list-modules", +		Func:  cmdListModules, +		Short: "List installed Caddy modules.", +		Long:  `List installed Caddy modules.`, +	}, + +	"environ": { +		Name:  "environ", +		Func:  cmdEnviron, +		Short: "Prints the environment as seen by Caddy.", +		Long:  `Prints the environment as seen by Caddy.`, +	}, + +	"adapt-config": { +		Name:  "adapt-config", +		Func:  cmdAdaptConfig, +		Usage: "--input <path> --adapter <name> [--pretty]", +		Short: "Adapts a configuration to Caddy's native JSON config structure", +		Long: ` +Adapts a configuration to Caddy's native JSON config structure and writes the +output to stdout, along with any warnings to stderr. If --pretty is specified, +the output will be formatted with indentation for human readability.`, +	},  } -func cmdStop() (int, error) { -	processList, err := ps.Processes() -	if err != nil { -		return caddy.ExitCodeFailedStartup, fmt.Errorf("listing processes: %v", err) -	} -	thisProcName := getProcessName() -	var found bool -	for _, p := range processList { -		// the process we're looking for should have the same name but different PID -		if p.Executable() == thisProcName && p.Pid() != os.Getpid() { -			found = true -			fmt.Printf("pid=%d\n", p.Pid()) - -			if err := gracefullyStopProcess(p.Pid()); err != nil { -				return caddy.ExitCodeFailedStartup, err -			} -		} -	} -	if !found { -		return caddy.ExitCodeFailedStartup, fmt.Errorf("Caddy is not running") +func init() { +	// the help command is special in that its func +	// refers to the commands map; thus, defining it +	// inline with the commands map's initialization +	// yields a compile-time error, so we have to +	// define this command separately +	commands["help"] = Command{ +		Name:  "help", +		Func:  cmdHelp, +		Usage: "<command>", +		Short: "Shows help for a Caddy subcommand.",  	} -	fmt.Println(" success") -	return caddy.ExitCodeSuccess, nil  } -func cmdReload() (int, error) { -	reloadCmd := flag.NewFlagSet("load", flag.ExitOnError) -	reloadCmdConfigFlag := reloadCmd.String("config", "", "Configuration file") -	reloadCmdConfigAdapterFlag := reloadCmd.String("config-adapter", "", "Name of config adapter to apply") -	reloadCmdAddrFlag := reloadCmd.String("address", "", "Address of the administration listener, if different from config") -	reloadCmd.Parse(os.Args[2:]) - -	// a configuration is required -	if *reloadCmdConfigFlag == "" { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("no configuration to load (use --config)") -	} - -	// get the config in caddy's native format -	config, err := loadConfig(*reloadCmdConfigFlag, *reloadCmdConfigAdapterFlag) -	if err != nil { -		return caddy.ExitCodeFailedStartup, err -	} - -	// get the address of the admin listener and craft endpoint URL -	adminAddr := *reloadCmdAddrFlag -	if adminAddr == "" { -		var tmpStruct struct { -			Admin caddy.AdminConfig `json:"admin"` -		} -		err = json.Unmarshal(config, &tmpStruct) -		if err != nil { -			return caddy.ExitCodeFailedStartup, -				fmt.Errorf("unmarshaling admin listener address from config: %v", err) -		} -		adminAddr = tmpStruct.Admin.Listen +// RegisterCommand registers the command cmd. +// cmd.Name must be unique and conform to the +// following format: +// +//    - lowercase +//    - alphanumeric and hyphen characters only +//    - cannot start or end with a hyphen +//    - hyphen cannot be adjacent to another hyphen +// +// This function panics if the name is already registered, +// if the name does not meet the described format, or if +// any of the fields are missing from cmd. +func RegisterCommand(cmd Command) { +	if cmd.Name == "" { +		panic("command name is required")  	} -	if adminAddr == "" { -		adminAddr = caddy.DefaultAdminListen +	if cmd.Func == nil { +		panic("command function missing")  	} -	adminEndpoint := fmt.Sprintf("http://%s/load", adminAddr) - -	// send the configuration to the instance -	resp, err := http.Post(adminEndpoint, "application/json", bytes.NewReader(config)) -	if err != nil { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("sending configuration to instance: %v", err) +	if cmd.Short == "" { +		panic("command short string is required")  	} -	defer resp.Body.Close() - -	// if it didn't work, let the user know -	if resp.StatusCode >= 400 { -		respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1024*10)) -		if err != nil { -			return caddy.ExitCodeFailedStartup, -				fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err) -		} -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody) +	if _, exists := commands[cmd.Name]; exists { +		panic("command already registered: " + cmd.Name)  	} - -	return caddy.ExitCodeSuccess, nil -} - -func cmdVersion() (int, error) { -	goModule := caddy.GoModule() -	if goModule.Sum != "" { -		// a build with a known version will also have a checksum -		fmt.Printf("%s %s\n", goModule.Version, goModule.Sum) -	} else { -		fmt.Println(goModule.Version) -	} -	return caddy.ExitCodeSuccess, nil -} - -func cmdListModules() (int, error) { -	for _, m := range caddy.Modules() { -		fmt.Println(m) -	} -	return caddy.ExitCodeSuccess, nil -} - -func cmdEnviron() (int, error) { -	for _, v := range os.Environ() { -		fmt.Println(v) +	if !commandNameRegex.MatchString(cmd.Name) { +		panic("invalid command name")  	} -	return caddy.ExitCodeSuccess, nil +	commands[cmd.Name] = cmd  } -func cmdAdaptConfig() (int, error) { -	adaptCmd := flag.NewFlagSet("adapt", flag.ExitOnError) -	adaptCmdAdapterFlag := adaptCmd.String("adapter", "", "Name of config adapter") -	adaptCmdInputFlag := adaptCmd.String("input", "", "Configuration file to adapt") -	adaptCmdPrettyFlag := adaptCmd.Bool("pretty", false, "Format the output for human readability") -	adaptCmd.Parse(os.Args[2:]) - -	if *adaptCmdAdapterFlag == "" || *adaptCmdInputFlag == "" { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("usage: caddy adapt-config --adapter <name> --input <file>") -	} - -	cfgAdapter := caddyconfig.GetAdapter(*adaptCmdAdapterFlag) -	if cfgAdapter == nil { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("unrecognized config adapter: %s", *adaptCmdAdapterFlag) -	} - -	input, err := ioutil.ReadFile(*adaptCmdInputFlag) -	if err != nil { -		return caddy.ExitCodeFailedStartup, -			fmt.Errorf("reading input file: %v", err) -	} - -	opts := make(map[string]interface{}) -	if *adaptCmdPrettyFlag { -		opts["pretty"] = "true" -	} - -	adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts) -	if err != nil { -		return caddy.ExitCodeFailedStartup, err -	} - -	// print warnings to stderr -	for _, warn := range warnings { -		msg := warn.Message -		if warn.Directive != "" { -			msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message) -		} -		log.Printf("[WARNING][%s] %s:%d: %s", *adaptCmdAdapterFlag, warn.File, warn.Line, msg) -	} - -	// print result to stdout -	fmt.Println(string(adaptedConfig)) - -	return caddy.ExitCodeSuccess, nil -} +var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`) | 
