mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 18:30:56 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			717 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			717 lines
		
	
	
		
			22 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"
 | |
| 	"context"
 | |
| 	"crypto/rand"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"os/exec"
 | |
| 	"runtime"
 | |
| 	"runtime/debug"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/aryann/difflib"
 | |
| 	"github.com/caddyserver/caddy/v2"
 | |
| 	"github.com/caddyserver/caddy/v2/caddyconfig"
 | |
| 	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
 | |
| 	"go.uber.org/zap"
 | |
| )
 | |
| 
 | |
| func cmdStart(fl Flags) (int, error) {
 | |
| 	startCmdConfigFlag := fl.String("config")
 | |
| 	startCmdConfigAdapterFlag := fl.String("adapter")
 | |
| 	startCmdPidfileFlag := fl.String("pidfile")
 | |
| 	startCmdWatchFlag := fl.Bool("watch")
 | |
| 	startCmdEnvfileFlag := fl.String("envfile")
 | |
| 
 | |
| 	// 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 startCmdEnvfileFlag != "" {
 | |
| 		cmd.Args = append(cmd.Args, "--envfile", startCmdEnvfileFlag)
 | |
| 	}
 | |
| 	if startCmdConfigAdapterFlag != "" {
 | |
| 		cmd.Args = append(cmd.Args, "--adapter", startCmdConfigAdapterFlag)
 | |
| 	}
 | |
| 	if startCmdWatchFlag {
 | |
| 		cmd.Args = append(cmd.Args, "--watch")
 | |
| 	}
 | |
| 	if startCmdPidfileFlag != "" {
 | |
| 		cmd.Args = append(cmd.Args, "--pidfile", startCmdPidfileFlag)
 | |
| 	}
 | |
| 	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 synchronously 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 !errors.Is(err, net.ErrClosed) {
 | |
| 					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) - Caddy is running in the background\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) {
 | |
| 	caddy.TrapSignals()
 | |
| 
 | |
| 	runCmdConfigFlag := fl.String("config")
 | |
| 	runCmdConfigAdapterFlag := fl.String("adapter")
 | |
| 	runCmdResumeFlag := fl.Bool("resume")
 | |
| 	runCmdLoadEnvfileFlag := fl.String("envfile")
 | |
| 	runCmdPrintEnvFlag := fl.Bool("environ")
 | |
| 	runCmdWatchFlag := fl.Bool("watch")
 | |
| 	runCmdPidfileFlag := fl.String("pidfile")
 | |
| 	runCmdPingbackFlag := fl.String("pingback")
 | |
| 
 | |
| 	// load all additional envs as soon as possible
 | |
| 	if runCmdLoadEnvfileFlag != "" {
 | |
| 		if err := loadEnvFromFile(runCmdLoadEnvfileFlag); err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup,
 | |
| 				fmt.Errorf("loading additional environment variables: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// if we are supposed to print the environment, do that first
 | |
| 	if runCmdPrintEnvFlag {
 | |
| 		printEnvironment()
 | |
| 	}
 | |
| 
 | |
| 	// load the config, depending on flags
 | |
| 	var config []byte
 | |
| 	var err error
 | |
| 	if runCmdResumeFlag {
 | |
| 		config, err = os.ReadFile(caddy.ConfigAutosavePath)
 | |
| 		if os.IsNotExist(err) {
 | |
| 			// not a bad error; just can't resume if autosave file doesn't exist
 | |
| 			caddy.Log().Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
 | |
| 			runCmdResumeFlag = false
 | |
| 		} else if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, err
 | |
| 		} else {
 | |
| 			if runCmdConfigFlag == "" {
 | |
| 				caddy.Log().Info("resuming from last configuration",
 | |
| 					zap.String("autosave_file", caddy.ConfigAutosavePath))
 | |
| 			} else {
 | |
| 				// if they also specified a config file, user should be aware that we're not
 | |
| 				// using it (doing so could lead to data/config loss by overwriting!)
 | |
| 				caddy.Log().Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
 | |
| 					zap.String("autosave_file", caddy.ConfigAutosavePath))
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	// we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
 | |
| 	var configFile string
 | |
| 	if !runCmdResumeFlag {
 | |
| 		config, configFile, err = LoadConfig(runCmdConfigFlag, runCmdConfigAdapterFlag)
 | |
| 		if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, err
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// run the initial config
 | |
| 	err = caddy.Load(config, true)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
 | |
| 	}
 | |
| 	caddy.Log().Info("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 := io.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)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// if enabled, reload config file automatically on changes
 | |
| 	// (this better only be used in dev!)
 | |
| 	if runCmdWatchFlag {
 | |
| 		go watchConfigFile(configFile, runCmdConfigAdapterFlag)
 | |
| 	}
 | |
| 
 | |
| 	// create pidfile
 | |
| 	if runCmdPidfileFlag != "" {
 | |
| 		err := caddy.PIDFile(runCmdPidfileFlag)
 | |
| 		if err != nil {
 | |
| 			caddy.Log().Error("unable to write PID file",
 | |
| 				zap.String("pidfile", runCmdPidfileFlag),
 | |
| 				zap.Error(err))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// warn if the environment does not provide enough information about the disk
 | |
| 	hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
 | |
| 		os.Getenv("XDG_CONFIG_HOME") != "" &&
 | |
| 		os.Getenv("XDG_CACHE_HOME") != ""
 | |
| 	switch runtime.GOOS {
 | |
| 	case "windows":
 | |
| 		if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG {
 | |
| 			caddy.Log().Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
 | |
| 		}
 | |
| 	case "plan9":
 | |
| 		if os.Getenv("home") == "" && !hasXDG {
 | |
| 			caddy.Log().Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
 | |
| 		}
 | |
| 	default:
 | |
| 		if os.Getenv("HOME") == "" && !hasXDG {
 | |
| 			caddy.Log().Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	select {}
 | |
| }
 | |
| 
 | |
| func cmdStop(fl Flags) (int, error) {
 | |
| 	addrFlag := fl.String("address")
 | |
| 	configFlag := fl.String("config")
 | |
| 	configAdapterFlag := fl.String("adapter")
 | |
| 
 | |
| 	adminAddr, err := DetermineAdminAPIAddress(addrFlag, nil, configFlag, configAdapterFlag)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
 | |
| 	if err != nil {
 | |
| 		caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	return caddy.ExitCodeSuccess, nil
 | |
| }
 | |
| 
 | |
| func cmdReload(fl Flags) (int, error) {
 | |
| 	configFlag := fl.String("config")
 | |
| 	configAdapterFlag := fl.String("adapter")
 | |
| 	addrFlag := fl.String("address")
 | |
| 	forceFlag := fl.Bool("force")
 | |
| 
 | |
| 	// get the config in caddy's native format
 | |
| 	config, configFile, err := LoadConfig(configFlag, configAdapterFlag)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 	if configFile == "" {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
 | |
| 	}
 | |
| 
 | |
| 	adminAddr, err := DetermineAdminAPIAddress(addrFlag, config, configFlag, configAdapterFlag)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// optionally force a config reload
 | |
| 	headers := make(http.Header)
 | |
| 	if forceFlag {
 | |
| 		headers.Set("Cache-Control", "must-revalidate")
 | |
| 	}
 | |
| 
 | |
| 	resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 
 | |
| 	return caddy.ExitCodeSuccess, nil
 | |
| }
 | |
| 
 | |
| func cmdVersion(_ Flags) (int, error) {
 | |
| 	_, full := caddy.Version()
 | |
| 	fmt.Println(full)
 | |
| 	return caddy.ExitCodeSuccess, nil
 | |
| }
 | |
| 
 | |
| func cmdBuildInfo(_ Flags) (int, error) {
 | |
| 	bi, ok := debug.ReadBuildInfo()
 | |
| 	if !ok {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
 | |
| 	}
 | |
| 	fmt.Println(bi)
 | |
| 	return caddy.ExitCodeSuccess, nil
 | |
| }
 | |
| 
 | |
| func cmdListModules(fl Flags) (int, error) {
 | |
| 	packages := fl.Bool("packages")
 | |
| 	versions := fl.Bool("versions")
 | |
| 	skipStandard := fl.Bool("skip-standard")
 | |
| 
 | |
| 	printModuleInfo := func(mi moduleInfo) {
 | |
| 		fmt.Print(mi.caddyModuleID)
 | |
| 		if versions && mi.goModule != nil {
 | |
| 			fmt.Print(" " + mi.goModule.Version)
 | |
| 		}
 | |
| 		if packages && mi.goModule != nil {
 | |
| 			fmt.Print(" " + mi.goModule.Path)
 | |
| 			if mi.goModule.Replace != nil {
 | |
| 				fmt.Print(" => " + mi.goModule.Replace.Path)
 | |
| 			}
 | |
| 		}
 | |
| 		if mi.err != nil {
 | |
| 			fmt.Printf(" [%v]", mi.err)
 | |
| 		}
 | |
| 		fmt.Println()
 | |
| 	}
 | |
| 
 | |
| 	// organize modules by whether they come with the standard distribution
 | |
| 	standard, nonstandard, unknown, err := getModules()
 | |
| 	if err != nil {
 | |
| 		// oh well, just print the module IDs and exit
 | |
| 		for _, m := range caddy.Modules() {
 | |
| 			fmt.Println(m)
 | |
| 		}
 | |
| 		return caddy.ExitCodeSuccess, nil
 | |
| 	}
 | |
| 
 | |
| 	// Standard modules (always shipped with Caddy)
 | |
| 	if !skipStandard {
 | |
| 		if len(standard) > 0 {
 | |
| 			for _, mod := range standard {
 | |
| 				printModuleInfo(mod)
 | |
| 			}
 | |
| 		}
 | |
| 		fmt.Printf("\n  Standard modules: %d\n", len(standard))
 | |
| 	}
 | |
| 
 | |
| 	// Non-standard modules (third party plugins)
 | |
| 	if len(nonstandard) > 0 {
 | |
| 		if len(standard) > 0 && !skipStandard {
 | |
| 			fmt.Println()
 | |
| 		}
 | |
| 		for _, mod := range nonstandard {
 | |
| 			printModuleInfo(mod)
 | |
| 		}
 | |
| 	}
 | |
| 	fmt.Printf("\n  Non-standard modules: %d\n", len(nonstandard))
 | |
| 
 | |
| 	// Unknown modules (couldn't get Caddy module info)
 | |
| 	if len(unknown) > 0 {
 | |
| 		if (len(standard) > 0 && !skipStandard) || len(nonstandard) > 0 {
 | |
| 			fmt.Println()
 | |
| 		}
 | |
| 		for _, mod := range unknown {
 | |
| 			printModuleInfo(mod)
 | |
| 		}
 | |
| 	}
 | |
| 	fmt.Printf("\n  Unknown modules: %d\n", len(unknown))
 | |
| 
 | |
| 	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 no input file was specified, try a default
 | |
| 	// Caddyfile if the Caddyfile adapter is plugged in
 | |
| 	if adaptCmdInputFlag == "" && caddyconfig.GetAdapter("caddyfile") != nil {
 | |
| 		_, err := os.Stat("Caddyfile")
 | |
| 		if err == nil {
 | |
| 			// default Caddyfile exists
 | |
| 			adaptCmdInputFlag = "Caddyfile"
 | |
| 			caddy.Log().Info("using adjacent Caddyfile")
 | |
| 		} else if !os.IsNotExist(err) {
 | |
| 			// default Caddyfile exists, but error accessing it
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("accessing default Caddyfile: %v", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if adaptCmdInputFlag == "" {
 | |
| 		return caddy.ExitCodeFailedStartup,
 | |
| 			fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
 | |
| 	}
 | |
| 	if adaptCmdAdapterFlag == "" {
 | |
| 		return caddy.ExitCodeFailedStartup,
 | |
| 			fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
 | |
| 	}
 | |
| 
 | |
| 	cfgAdapter := caddyconfig.GetAdapter(adaptCmdAdapterFlag)
 | |
| 	if cfgAdapter == nil {
 | |
| 		return caddy.ExitCodeFailedStartup,
 | |
| 			fmt.Errorf("unrecognized config adapter: %s", adaptCmdAdapterFlag)
 | |
| 	}
 | |
| 
 | |
| 	input, err := os.ReadFile(adaptCmdInputFlag)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup,
 | |
| 			fmt.Errorf("reading input file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	opts := map[string]any{"filename": adaptCmdInputFlag}
 | |
| 
 | |
| 	adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 
 | |
| 	if adaptCmdPrettyFlag {
 | |
| 		var prettyBuf bytes.Buffer
 | |
| 		err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
 | |
| 		if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, err
 | |
| 		}
 | |
| 		adaptedConfig = prettyBuf.Bytes()
 | |
| 	}
 | |
| 
 | |
| 	// print result to stdout
 | |
| 	fmt.Println(string(adaptedConfig))
 | |
| 
 | |
| 	// print warnings to stderr
 | |
| 	for _, warn := range warnings {
 | |
| 		msg := warn.Message
 | |
| 		if warn.Directive != "" {
 | |
| 			msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
 | |
| 		}
 | |
| 		caddy.Log().Named(adaptCmdAdapterFlag).Warn(msg,
 | |
| 			zap.String("file", warn.File),
 | |
| 			zap.Int("line", warn.Line))
 | |
| 	}
 | |
| 
 | |
| 	// 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 := LoadConfig(validateCmdConfigFlag, validateCmdAdapterFlag)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup, err
 | |
| 	}
 | |
| 	input = caddy.RemoveMetaFields(input)
 | |
| 
 | |
| 	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 cmdFmt(fl Flags) (int, error) {
 | |
| 	formatCmdConfigFile := fl.Arg(0)
 | |
| 	if formatCmdConfigFile == "" {
 | |
| 		formatCmdConfigFile = "Caddyfile"
 | |
| 	}
 | |
| 
 | |
| 	// as a special case, read from stdin if the file name is "-"
 | |
| 	if formatCmdConfigFile == "-" {
 | |
| 		input, err := io.ReadAll(os.Stdin)
 | |
| 		if err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup,
 | |
| 				fmt.Errorf("reading stdin: %v", err)
 | |
| 		}
 | |
| 		fmt.Print(string(caddyfile.Format(input)))
 | |
| 		return caddy.ExitCodeSuccess, nil
 | |
| 	}
 | |
| 
 | |
| 	input, err := os.ReadFile(formatCmdConfigFile)
 | |
| 	if err != nil {
 | |
| 		return caddy.ExitCodeFailedStartup,
 | |
| 			fmt.Errorf("reading input file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	output := caddyfile.Format(input)
 | |
| 
 | |
| 	if fl.Bool("overwrite") {
 | |
| 		if err := os.WriteFile(formatCmdConfigFile, output, 0600); err != nil {
 | |
| 			return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
 | |
| 		}
 | |
| 		return caddy.ExitCodeSuccess, nil
 | |
| 	}
 | |
| 
 | |
| 	if fl.Bool("diff") {
 | |
| 		diff := difflib.Diff(
 | |
| 			strings.Split(string(input), "\n"),
 | |
| 			strings.Split(string(output), "\n"))
 | |
| 		for _, d := range diff {
 | |
| 			switch d.Delta {
 | |
| 			case difflib.Common:
 | |
| 				fmt.Printf("  %s\n", d.Payload)
 | |
| 			case difflib.LeftOnly:
 | |
| 				fmt.Printf("- %s\n", d.Payload)
 | |
| 			case difflib.RightOnly:
 | |
| 				fmt.Printf("+ %s\n", d.Payload)
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		fmt.Print(string(output))
 | |
| 	}
 | |
| 
 | |
| 	if warning, diff := caddyfile.FormattingDifference(formatCmdConfigFile, input); diff {
 | |
| 		return caddy.ExitCodeFailedStartup, fmt.Errorf("%s:%d: Caddyfile input is not formatted", warning.File, warning.Line)
 | |
| 	}
 | |
| 
 | |
| 	return caddy.ExitCodeSuccess, nil
 | |
| }
 | |
| 
 | |
| // AdminAPIRequest makes an API request according to the CLI flags given,
 | |
| // with the given HTTP method and request URI. If body is non-nil, it will
 | |
| // be assumed to be Content-Type application/json. The caller should close
 | |
| // the response body. Should only be used by Caddy CLI commands which
 | |
| // need to interact with a running instance of Caddy via the admin API.
 | |
| func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
 | |
| 	parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
 | |
| 	if err != nil || parsedAddr.PortRangeSize() > 1 {
 | |
| 		return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
 | |
| 	}
 | |
| 	origin := "http://" + parsedAddr.JoinHostPort(0)
 | |
| 	if parsedAddr.IsUnixNetwork() {
 | |
| 		origin = "http://unixsocket" // hack so that http.NewRequest() is happy
 | |
| 	}
 | |
| 
 | |
| 	// form the request
 | |
| 	req, err := http.NewRequest(method, origin+uri, body)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("making request: %v", err)
 | |
| 	}
 | |
| 	if parsedAddr.IsUnixNetwork() {
 | |
| 		// When listening on a unix socket, the admin endpoint doesn't
 | |
| 		// accept any Host header because there is no host:port for
 | |
| 		// a unix socket's address. The server's host check is fairly
 | |
| 		// strict for security reasons, so we don't allow just any
 | |
| 		// Host header. For unix sockets, the Host header must be
 | |
| 		// empty. Unfortunately, Go makes it impossible to make HTTP
 | |
| 		// requests with an empty Host header... except with this one
 | |
| 		// weird trick. (Hopefully they don't fix it. It's already
 | |
| 		// hard enough to use HTTP over unix sockets.)
 | |
| 		//
 | |
| 		// An equivalent curl command would be something like:
 | |
| 		// $ curl --unix-socket caddy.sock http:/:$REQUEST_URI
 | |
| 		req.URL.Host = " "
 | |
| 		req.Host = ""
 | |
| 	} else {
 | |
| 		req.Header.Set("Origin", origin)
 | |
| 	}
 | |
| 	if body != nil {
 | |
| 		req.Header.Set("Content-Type", "application/json")
 | |
| 	}
 | |
| 	for k, v := range headers {
 | |
| 		req.Header[k] = v
 | |
| 	}
 | |
| 
 | |
| 	// make an HTTP client that dials our network type, since admin
 | |
| 	// endpoints aren't always TCP, which is what the default transport
 | |
| 	// expects; reuse is not of particular concern here
 | |
| 	client := http.Client{
 | |
| 		Transport: &http.Transport{
 | |
| 			DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
 | |
| 				return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0))
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	resp, err := client.Do(req)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("performing request: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	// if it didn't work, let the user know
 | |
| 	if resp.StatusCode >= 400 {
 | |
| 		respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*10))
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
 | |
| 		}
 | |
| 		return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
 | |
| 	}
 | |
| 
 | |
| 	return resp, nil
 | |
| }
 | |
| 
 | |
| // DetermineAdminAPIAddress determines which admin API endpoint address should
 | |
| // be used based on the inputs. By priority: if `address` is specified, then
 | |
| // it is returned; if `config` is specified, then that config will be used for
 | |
| // finding the admin address; if `configFile` (and `configAdapter`) are specified,
 | |
| // then that config will be loaded to find the admin address; otherwise, the
 | |
| // default admin listen address will be returned.
 | |
| func DetermineAdminAPIAddress(address string, config []byte, configFile, configAdapter string) (string, error) {
 | |
| 	// Prefer the address if specified and non-empty
 | |
| 	if address != "" {
 | |
| 		return address, nil
 | |
| 	}
 | |
| 
 | |
| 	// Try to load the config from file if specified, with the given adapter name
 | |
| 	if configFile != "" {
 | |
| 		var loadedConfigFile string
 | |
| 		var err error
 | |
| 
 | |
| 		// use the provided loaded config if non-empty
 | |
| 		// otherwise, load it from the specified file/adapter
 | |
| 		loadedConfig := config
 | |
| 		if len(loadedConfig) == 0 {
 | |
| 			// get the config in caddy's native format
 | |
| 			loadedConfig, loadedConfigFile, err = LoadConfig(configFile, configAdapter)
 | |
| 			if err != nil {
 | |
| 				return "", err
 | |
| 			}
 | |
| 			if loadedConfigFile == "" {
 | |
| 				return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory")
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// get the address of the admin listener from the config
 | |
| 		if len(loadedConfig) > 0 {
 | |
| 			var tmpStruct struct {
 | |
| 				Admin caddy.AdminConfig `json:"admin"`
 | |
| 			}
 | |
| 			err := json.Unmarshal(loadedConfig, &tmpStruct)
 | |
| 			if err != nil {
 | |
| 				return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
 | |
| 			}
 | |
| 			if tmpStruct.Admin.Listen != "" {
 | |
| 				return tmpStruct.Admin.Listen, nil
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Fallback to the default listen address otherwise
 | |
| 	return caddy.DefaultAdminListen, nil
 | |
| }
 | |
| 
 | |
| type moduleInfo struct {
 | |
| 	caddyModuleID string
 | |
| 	goModule      *debug.Module
 | |
| 	err           error
 | |
| }
 | 
