mirror of
https://github.com/caddyserver/caddy.git
synced 2024-11-25 09:40:13 +08:00
787f6b257f
* chore: replace `os.ErrNotExist` with `fs.ErrNotExist` * check against permission error from `io/fs` package
803 lines
24 KiB
Go
803 lines
24 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"
|
|
"io/fs"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"strings"
|
|
|
|
"github.com/aryann/difflib"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
"github.com/caddyserver/caddy/v2/internal"
|
|
)
|
|
|
|
func cmdStart(fl Flags) (int, error) {
|
|
configFlag := fl.String("config")
|
|
configAdapterFlag := fl.String("adapter")
|
|
pidfileFlag := fl.String("pidfile")
|
|
watchFlag := fl.Bool("watch")
|
|
|
|
var err error
|
|
var envfileFlag []string
|
|
envfileFlag, err = fl.GetStringSlice("envfile")
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("reading envfile flag: %v", err)
|
|
}
|
|
|
|
// 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 configFlag != "" {
|
|
cmd.Args = append(cmd.Args, "--config", configFlag)
|
|
}
|
|
|
|
for _, envfile := range envfileFlag {
|
|
cmd.Args = append(cmd.Args, "--envfile", envfile)
|
|
}
|
|
if configAdapterFlag != "" {
|
|
cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
|
|
}
|
|
if watchFlag {
|
|
cmd.Args = append(cmd.Args, "--watch")
|
|
}
|
|
if pidfileFlag != "" {
|
|
cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
|
|
}
|
|
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()
|
|
|
|
configFlag := fl.String("config")
|
|
configAdapterFlag := fl.String("adapter")
|
|
resumeFlag := fl.Bool("resume")
|
|
printEnvFlag := fl.Bool("environ")
|
|
watchFlag := fl.Bool("watch")
|
|
pidfileFlag := fl.String("pidfile")
|
|
pingbackFlag := fl.String("pingback")
|
|
|
|
// load all additional envs as soon as possible
|
|
err := handleEnvFileFlag(fl)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
// if we are supposed to print the environment, do that first
|
|
if printEnvFlag {
|
|
printEnvironment()
|
|
}
|
|
|
|
// load the config, depending on flags
|
|
var config []byte
|
|
if resumeFlag {
|
|
config, err = os.ReadFile(caddy.ConfigAutosavePath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
// 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))
|
|
resumeFlag = false
|
|
} else if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
} else {
|
|
if configFlag == "" {
|
|
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 !resumeFlag {
|
|
config, configFile, err = LoadConfig(configFlag, configAdapterFlag)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
}
|
|
|
|
// create pidfile now, in case loading config takes a while (issue #5477)
|
|
if pidfileFlag != "" {
|
|
err := caddy.PIDFile(pidfileFlag)
|
|
if err != nil {
|
|
caddy.Log().Error("unable to write PID file",
|
|
zap.String("pidfile", pidfileFlag),
|
|
zap.Error(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 pingbackFlag != "" {
|
|
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", pingbackFlag)
|
|
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", pingbackFlag, err)
|
|
}
|
|
}
|
|
|
|
// if enabled, reload config file automatically on changes
|
|
// (this better only be used in dev!)
|
|
if watchFlag {
|
|
go watchConfigFile(configFile, configAdapterFlag)
|
|
}
|
|
|
|
// 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) {
|
|
addressFlag := fl.String("address")
|
|
configFlag := fl.String("config")
|
|
configAdapterFlag := fl.String("adapter")
|
|
|
|
adminAddr, err := DetermineAdminAPIAddress(addressFlag, 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")
|
|
addressFlag := 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(addressFlag, 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(fl Flags) (int, error) {
|
|
// load all additional envs as soon as possible
|
|
err := handleEnvFileFlag(fl)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
printEnvironment()
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
func cmdAdaptConfig(fl Flags) (int, error) {
|
|
inputFlag := fl.String("config")
|
|
adapterFlag := fl.String("adapter")
|
|
prettyFlag := fl.Bool("pretty")
|
|
validateFlag := fl.Bool("validate")
|
|
|
|
var err error
|
|
inputFlag, err = configFileWithRespectToDefault(caddy.Log(), inputFlag)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
// load all additional envs as soon as possible
|
|
err = handleEnvFileFlag(fl)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
if adapterFlag == "" {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
|
|
}
|
|
|
|
cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
|
|
if cfgAdapter == nil {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
|
|
}
|
|
|
|
input, err := os.ReadFile(inputFlag)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("reading input file: %v", err)
|
|
}
|
|
|
|
opts := map[string]any{"filename": inputFlag}
|
|
|
|
adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
if prettyFlag {
|
|
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(adapterFlag).Warn(msg,
|
|
zap.String("file", warn.File),
|
|
zap.Int("line", warn.Line))
|
|
}
|
|
|
|
// validate output if requested
|
|
if validateFlag {
|
|
var cfg *caddy.Config
|
|
err = caddy.StrictUnmarshalJSON(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) {
|
|
configFlag := fl.String("config")
|
|
adapterFlag := fl.String("adapter")
|
|
|
|
// load all additional envs as soon as possible
|
|
err := handleEnvFileFlag(fl)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
|
|
// use default config and ensure a config file is specified
|
|
configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
if configFlag == "" {
|
|
return caddy.ExitCodeFailedStartup,
|
|
fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
|
|
}
|
|
|
|
input, _, err := LoadConfig(configFlag, adapterFlag)
|
|
if err != nil {
|
|
return caddy.ExitCodeFailedStartup, err
|
|
}
|
|
input = caddy.RemoveMetaFields(input)
|
|
|
|
var cfg *caddy.Config
|
|
err = caddy.StrictUnmarshalJSON(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) {
|
|
configFile := fl.Arg(0)
|
|
if configFile == "" {
|
|
configFile = "Caddyfile"
|
|
}
|
|
|
|
// as a special case, read from stdin if the file name is "-"
|
|
if configFile == "-" {
|
|
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(configFile)
|
|
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(configFile, output, 0o600); 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(configFile, input); diff {
|
|
return caddy.ExitCodeFailedStartup, fmt.Errorf(`%s:%d: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options`,
|
|
warning.File,
|
|
warning.Line,
|
|
)
|
|
}
|
|
|
|
return caddy.ExitCodeSuccess, nil
|
|
}
|
|
|
|
// handleEnvFileFlag loads the environment variables from the given --envfile
|
|
// flag if specified. This should be called as early in the command function.
|
|
func handleEnvFileFlag(fl Flags) error {
|
|
var err error
|
|
var envfileFlag []string
|
|
envfileFlag, err = fl.GetStringSlice("envfile")
|
|
if err != nil {
|
|
return fmt.Errorf("reading envfile flag: %v", err)
|
|
}
|
|
|
|
for _, envfile := range envfileFlag {
|
|
if err := loadEnvFromFile(envfile); err != nil {
|
|
return fmt.Errorf("loading additional environment variables: %v", err)
|
|
}
|
|
}
|
|
|
|
return 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://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy
|
|
|
|
// the unix address at this point might still contain the optional
|
|
// unix socket permissions, which are part of the address/host.
|
|
// those need to be removed first, as they aren't part of the
|
|
// resulting unix file path
|
|
addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parsedAddr.Host = addr
|
|
}
|
|
|
|
// 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() {
|
|
// We used to conform to RFC 2616 Section 14.26 which requires
|
|
// an empty host header when there is no host, as is the case
|
|
// with unix sockets. However, Go required a Host value so we
|
|
// used a hack of a space character as the host (it would see
|
|
// the Host was non-empty, then trim the space later). As of
|
|
// Go 1.20.6 (July 2023), this hack no longer works. See:
|
|
// https://github.com/golang/go/issues/60374
|
|
// See also the discussion here:
|
|
// https://github.com/golang/go/issues/61431
|
|
//
|
|
// After that, we now require a Host value of either 127.0.0.1
|
|
// or ::1 if one is set. Above I choose to use 127.0.0.1. Even
|
|
// though the value should be completely irrelevant (it could be
|
|
// "srldkjfsd"), if for some reason the Host *is* used, at least
|
|
// we can have some reasonable assurance it will stay on the local
|
|
// machine and that browsers, if they ever allow access to unix
|
|
// sockets, can still enforce CORS, ensuring it is still coming
|
|
// from the local machine.
|
|
} 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
|
|
}
|
|
|
|
// configFileWithRespectToDefault returns the filename to use for loading the config, based
|
|
// on whether a config file is already specified and a supported default config file exists.
|
|
func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
|
|
const defaultCaddyfile = "Caddyfile"
|
|
|
|
// if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
|
|
if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
|
|
_, err := os.Stat(defaultCaddyfile)
|
|
if err == nil {
|
|
// default Caddyfile exists
|
|
if logger != nil {
|
|
logger.Info("using adjacent Caddyfile")
|
|
}
|
|
return defaultCaddyfile, nil
|
|
}
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
// problem checking
|
|
return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
|
|
}
|
|
}
|
|
|
|
// default config file does not exist or is irrelevant
|
|
return configFile, nil
|
|
}
|
|
|
|
type moduleInfo struct {
|
|
caddyModuleID string
|
|
goModule *debug.Module
|
|
err error
|
|
}
|