mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-27 22:23:48 +03:00
3c90e370a4
This commit goes a long way toward making automated documentation of Caddy config and Caddy modules possible. It's a broad, sweeping change, but mostly internal. It allows us to automatically generate docs for all Caddy modules (including future third-party ones) and make them viewable on a web page; it also doubles as godoc comments. As such, this commit makes significant progress in migrating the docs from our temporary wiki page toward our new website which is still under construction. With this change, all host modules will use ctx.LoadModule() and pass in both the struct pointer and the field name as a string. This allows the reflect package to read the struct tag from that field so that it can get the necessary information like the module namespace and the inline key. This has the nice side-effect of unifying the code and documentation. It also simplifies module loading, and handles several variations on field types for raw module fields (i.e. variations on json.RawMessage, such as arrays and maps). I also renamed ModuleInfo.Name -> ModuleInfo.ID, to make it clear that the ID is the "full name" which includes both the module namespace and the name. This clarity is helpful when describing module hierarchy. As of this change, Caddy modules are no longer an experimental design. I think the architecture is good enough to go forward.
565 lines
16 KiB
Go
565 lines
16 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddycmd
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"reflect"
|
|
"runtime/debug"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
"github.com/keybase/go-ps"
|
|
"github.com/mholt/certmagic"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func cmdStart(fl Flags) (int, error) {
|
|
startCmdConfigFlag := fl.String("config")
|
|
startCmdConfigAdapterFlag := fl.String("adapter")
|
|
|
|
// 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()
|
|
|
|
// 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, "--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
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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()
|
|
}()
|
|
|
|
// 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
|
|
}
|
|
|
|
func cmdRun(fl Flags) (int, error) {
|
|
runCmdConfigFlag := fl.String("config")
|
|
runCmdConfigAdapterFlag := fl.String("adapter")
|
|
runCmdPrintEnvFlag := fl.Bool("environ")
|
|
runCmdPingbackFlag := fl.String("pingback")
|
|
|
|
// if we are supposed to print the environment, do that first
|
|
if runCmdPrintEnvFlag {
|
|
printEnvironment()
|
|
}
|
|
|
|
// 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
|
|
|
|
// run the initial config
|
|
err = caddy.Load(config, true)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
|
|
}
|
|
if len(config) > 0 {
|
|
caddy.Log().Named("admin").Info("Caddy 2 serving initial configuration")
|
|
}
|
|
|
|
// 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 {}
|
|
}
|
|
|
|
func cmdStop(fl Flags) (int, error) {
|
|
stopCmdAddrFlag := fl.String("address")
|
|
|
|
adminAddr := caddy.DefaultAdminListen
|
|
if stopCmdAddrFlag != "" {
|
|
adminAddr = stopCmdAddrFlag
|
|
}
|
|
stopEndpoint := fmt.Sprintf("http://%s/stop", adminAddr)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, stopEndpoint, nil)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
|
|
}
|
|
req.Header.Set("Origin", adminAddr)
|
|
|
|
err = apiRequest(req)
|
|
if err != nil {
|
|
// if the caddy instance doesn't have an API listener set up,
|
|
// or we are unable to reach it for some reason, try signaling it
|
|
|
|
caddy.Log().Warn("unable to use API to stop instance; will try to signal the process",
|
|
zap.String("endpoint", stopEndpoint),
|
|
zap.Error(err),
|
|
)
|
|
|
|
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 as us 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")
|
|
}
|
|
|
|
fmt.Println(" success")
|
|
}
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdReload(fl Flags) (int, error) {
|
|
reloadCmdConfigFlag := fl.String("config")
|
|
reloadCmdConfigAdapterFlag := fl.String("adapter")
|
|
reloadCmdAddrFlag := fl.String("address")
|
|
|
|
// 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
|
|
}
|
|
if adminAddr == "" {
|
|
adminAddr = caddy.DefaultAdminListen
|
|
}
|
|
loadEndpoint := fmt.Sprintf("http://%s/load", adminAddr)
|
|
|
|
// prepare the request to update the configuration
|
|
req, err := http.NewRequest(http.MethodPost, loadEndpoint, bytes.NewReader(config))
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("making request: %v", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Origin", adminAddr)
|
|
|
|
err = apiRequest(req)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
|
|
}
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdVersion(_ Flags) (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(fl Flags) (int, error) {
|
|
versions := fl.Bool("versions")
|
|
|
|
bi, ok := debug.ReadBuildInfo()
|
|
if !ok || !versions {
|
|
// if there's no build information,
|
|
// just print out the modules
|
|
for _, m := range caddy.Modules() {
|
|
fmt.Println(m)
|
|
}
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
for _, modID := range caddy.Modules() {
|
|
modInfo, err := caddy.GetModule(modID)
|
|
if err != nil {
|
|
// that's weird
|
|
fmt.Println(modID)
|
|
continue
|
|
}
|
|
|
|
// to get the Caddy plugin's version info, we need to know
|
|
// the package that the Caddy module's value comes from; we
|
|
// can use reflection but we need a non-pointer value (I'm
|
|
// not sure why), and since New() should return a pointer
|
|
// value, we need to dereference it first
|
|
iface := interface{}(modInfo.New())
|
|
if rv := reflect.ValueOf(iface); rv.Kind() == reflect.Ptr {
|
|
iface = reflect.New(reflect.TypeOf(iface).Elem()).Elem().Interface()
|
|
}
|
|
modPkgPath := reflect.TypeOf(iface).PkgPath()
|
|
|
|
// now we find the Go module that the Caddy module's package
|
|
// belongs to; we assume the Caddy module package path will
|
|
// be prefixed by its Go module path, and we will choose the
|
|
// longest matching prefix in case there are nested modules
|
|
var matched *debug.Module
|
|
for _, dep := range bi.Deps {
|
|
if strings.HasPrefix(modPkgPath, dep.Path) {
|
|
if matched == nil || len(dep.Path) > len(matched.Path) {
|
|
matched = dep
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we could find no matching module, just print out
|
|
// the module ID instead
|
|
if matched == nil {
|
|
fmt.Println(modID)
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("%s %s\n", modID, matched.Version)
|
|
}
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdEnviron(_ Flags) (int, error) {
|
|
printEnvironment()
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdAdaptConfig(fl Flags) (int, error) {
|
|
adaptCmdInputFlag := fl.String("config")
|
|
adaptCmdAdapterFlag := fl.String("adapter")
|
|
adaptCmdPrettyFlag := fl.Bool("pretty")
|
|
adaptCmdValidateFlag := fl.Bool("validate")
|
|
|
|
if adaptCmdAdapterFlag == "" || adaptCmdInputFlag == "" {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("--adapter and --config flags are required")
|
|
}
|
|
|
|
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)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", adaptCmdAdapterFlag, warn.File, warn.Line, msg)
|
|
}
|
|
|
|
// print result to stdout
|
|
fmt.Println(string(adaptedConfig))
|
|
|
|
// validate output if requested
|
|
if adaptCmdValidateFlag {
|
|
var cfg *caddy.Config
|
|
err = json.Unmarshal(adaptedConfig, &cfg)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
|
|
}
|
|
err = caddy.Validate(cfg)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("validation: %v", err)
|
|
}
|
|
}
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdValidateConfig(fl Flags) (int, error) {
|
|
validateCmdConfigFlag := fl.String("config")
|
|
validateCmdAdapterFlag := fl.String("adapter")
|
|
|
|
input, err := ioutil.ReadFile(validateCmdConfigFlag)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("reading input file: %v", err)
|
|
}
|
|
|
|
if validateCmdAdapterFlag != "" {
|
|
cfgAdapter := caddyconfig.GetAdapter(validateCmdAdapterFlag)
|
|
if cfgAdapter == nil {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("unrecognized config adapter: %s", validateCmdAdapterFlag)
|
|
}
|
|
|
|
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, nil)
|
|
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)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "[WARNING][%s] %s:%d: %s\n", validateCmdAdapterFlag, warn.File, warn.Line, msg)
|
|
}
|
|
|
|
input = adaptedConfig
|
|
}
|
|
|
|
var cfg *caddy.Config
|
|
err = json.Unmarshal(input, &cfg)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
|
|
}
|
|
|
|
err = caddy.Validate(cfg)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
fmt.Println("Valid configuration")
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdHelp(fl Flags) (int, error) {
|
|
const fullDocs = `Full documentation is available at:
|
|
https://github.com/caddyserver/caddy/wiki/v2:-Documentation`
|
|
|
|
args := fl.Args()
|
|
if len(args) == 0 {
|
|
s := `Caddy is an extensible server platform.
|
|
|
|
usage:
|
|
caddy <command> [<args...>]
|
|
|
|
commands:
|
|
`
|
|
keys := make([]string, 0, len(commands))
|
|
for k := range commands {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, k := range keys {
|
|
cmd := commands[k]
|
|
short := strings.TrimSuffix(cmd.Short, ".")
|
|
s += fmt.Sprintf(" %-15s %s\n", cmd.Name, short)
|
|
}
|
|
|
|
s += "\nUse 'caddy help <command>' for more information about a command.\n"
|
|
s += "\n" + fullDocs + "\n"
|
|
|
|
fmt.Print(s)
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
} else if len(args) > 1 {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("can only give help with one command")
|
|
}
|
|
|
|
subcommand, ok := commands[args[0]]
|
|
if !ok {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf("unknown command: %s", args[0])
|
|
}
|
|
|
|
helpText := strings.TrimSpace(subcommand.Long)
|
|
if helpText == "" {
|
|
helpText = subcommand.Short
|
|
if !strings.HasSuffix(helpText, ".") {
|
|
helpText += "."
|
|
}
|
|
}
|
|
|
|
result := fmt.Sprintf("%s\n\nusage:\n caddy %s %s\n",
|
|
helpText,
|
|
subcommand.Name,
|
|
strings.TrimSpace(subcommand.Usage),
|
|
)
|
|
|
|
if help := flagHelp(subcommand.Flags); help != "" {
|
|
result += fmt.Sprintf("\nflags:\n%s", help)
|
|
}
|
|
|
|
result += "\n" + fullDocs + "\n"
|
|
|
|
fmt.Print(result)
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func apiRequest(req *http.Request) error {
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("performing request: %v", err)
|
|
}
|
|
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 fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
|
|
}
|
|
return fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
|
|
}
|
|
|
|
return nil
|
|
}
|