@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 10, 23:45 UTC

master
go 1,111 lines 34.2 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package caddycmd
16
17 import (
18 "bytes"
19 "context"
20 "crypto/rand"
21 "encoding/json"
22 "errors"
23 "fmt"
24 "io"
25 "io/fs"
26 "log"
27 "maps"
28 "net"
29 "net/http"
30 "os"
31 "os/exec"
32 "path/filepath"
33 "runtime"
34 "runtime/debug"
35 "strings"
36
37 "github.com/aryann/difflib"
38 "go.uber.org/zap"
39
40 "github.com/caddyserver/caddy/v2"
41 "github.com/caddyserver/caddy/v2/caddyconfig"
42 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
43 "github.com/caddyserver/caddy/v2/internal"
44 )
45
46 func cmdStart(fl Flags) (int, error) {
47 configFlag := fl.String("config")
48 configAdapterFlag := fl.String("adapter")
49 pidfileFlag := fl.String("pidfile")
50 watchFlag := fl.Bool("watch")
51
52 var err error
53 var envfileFlag []string
54 envfileFlag, err = fl.GetStringSlice("envfile")
55 if err != nil {
56 return caddy.ExitCodeFailedStartup,
57 fmt.Errorf("reading envfile flag: %v", err)
58 }
59
60 // open a listener to which the child process will connect when
61 // it is ready to confirm that it has successfully started
62 ln, err := listenTCPForPingback(net.Listen)
63 if err != nil {
64 return caddy.ExitCodeFailedStartup,
65 fmt.Errorf("opening listener for success confirmation: %v", err)
66 }
67 defer ln.Close()
68
69 // craft the command with a pingback address and with a
70 // pipe for its stdin, so we can tell it our confirmation
71 // code that we expect so that some random port scan at
72 // the most unfortunate time won't fool us into thinking
73 // the child succeeded (i.e. the alternative is to just
74 // wait for any connection on our listener, but better to
75 // ensure it's the process we're expecting - we can be
76 // sure by giving it some random bytes and having it echo
77 // them back to us)
78 cmd := exec.Command(os.Args[0], "run", "--pingback", ln.Addr().String()) //nolint:gosec // no command injection that I can determine...
79 // we should be able to run caddy in relative paths
80 if errors.Is(cmd.Err, exec.ErrDot) {
81 cmd.Err = nil
82 }
83 if configFlag != "" {
84 cmd.Args = append(cmd.Args, "--config", configFlag)
85 }
86
87 for _, envfile := range envfileFlag {
88 cmd.Args = append(cmd.Args, "--envfile", envfile)
89 }
90 if configAdapterFlag != "" {
91 cmd.Args = append(cmd.Args, "--adapter", configAdapterFlag)
92 }
93 if watchFlag {
94 cmd.Args = append(cmd.Args, "--watch")
95 }
96 if pidfileFlag != "" {
97 cmd.Args = append(cmd.Args, "--pidfile", pidfileFlag)
98 }
99 stdinPipe, err := cmd.StdinPipe()
100 if err != nil {
101 return caddy.ExitCodeFailedStartup,
102 fmt.Errorf("creating stdin pipe: %v", err)
103 }
104 cmd.Stdout = os.Stdout
105 cmd.Stderr = os.Stderr
106
107 // generate the random bytes we'll send to the child process
108 expect := make([]byte, 32)
109 _, err = rand.Read(expect)
110 if err != nil {
111 return caddy.ExitCodeFailedStartup,
112 fmt.Errorf("generating random confirmation bytes: %v", err)
113 }
114
115 // begin writing the confirmation bytes to the child's
116 // stdin; use a goroutine since the child hasn't been
117 // started yet, and writing synchronously would result
118 // in a deadlock
119 go func() {
120 _, _ = stdinPipe.Write(expect)
121 stdinPipe.Close()
122 }()
123
124 // start the process
125 err = cmd.Start()
126 if err != nil {
127 return caddy.ExitCodeFailedStartup,
128 fmt.Errorf("starting caddy process: %v", err)
129 }
130
131 // there are two ways we know we're done: either
132 // the process will connect to our listener, or
133 // it will exit with an error
134 success, exit := make(chan struct{}), make(chan error)
135
136 // in one goroutine, we await the success of the child process
137 go func() {
138 for {
139 conn, err := ln.Accept()
140 if err != nil {
141 if !errors.Is(err, net.ErrClosed) {
142 log.Println(err)
143 }
144 break
145 }
146 err = handlePingbackConn(conn, expect)
147 if err == nil {
148 close(success)
149 break
150 }
151 log.Println(err)
152 }
153 }()
154
155 // in another goroutine, we await the failure of the child process
156 go func() {
157 err := cmd.Wait() // don't send on this line! Wait blocks, but send starts before it unblocks
158 exit <- err // sending on separate line ensures select won't trigger until after Wait unblocks
159 }()
160
161 // when one of the goroutines unblocks, we're done and can exit
162 select {
163 case <-success:
164 fmt.Printf("Successfully started Caddy (pid=%d) - Caddy is running in the background\n", cmd.Process.Pid)
165 case err := <-exit:
166 return caddy.ExitCodeFailedStartup,
167 fmt.Errorf("caddy process exited with error: %v", err)
168 }
169
170 return caddy.ExitCodeSuccess, nil
171 }
172
173 type tcpListenFunc func(network, address string) (net.Listener, error)
174
175 func listenTCPForPingback(listen tcpListenFunc) (net.Listener, error) {
176 ln, ipv4Err := listen("tcp4", "127.0.0.1:0")
177 if ipv4Err == nil {
178 return ln, nil
179 }
180
181 ln, ipv6Err := listen("tcp6", "[::1]:0")
182 if ipv6Err == nil {
183 return ln, nil
184 }
185
186 return nil, fmt.Errorf("listen on 127.0.0.1:0: %v; listen on [::1]:0: %v", ipv4Err, ipv6Err)
187 }
188
189 func cmdRun(fl Flags) (int, error) {
190 caddy.TrapSignals()
191
192 // set up buffered logging for early startup
193 // so that we can hold onto logs until after
194 // the config is loaded (or fails to load)
195 // so that we can write the logs to the user's
196 // configured output.
197 logger, defaultLogger, _ := caddy.BufferedLog()
198 defer caddy.UnbufferDefaultLogger(defaultLogger)
199
200 undoMaxProcs := setResourceLimits(logger)
201 defer undoMaxProcs()
202 // release the local reference to the undo function so it can be GC'd;
203 // the deferred call above has already captured the actual function value.
204 undoMaxProcs = nil //nolint:ineffassign,wastedassign
205
206 configFlag := fl.String("config")
207 configAdapterFlag := fl.String("adapter")
208 resumeFlag := fl.Bool("resume")
209 printEnvFlag := fl.Bool("environ")
210 watchFlag := fl.Bool("watch")
211 pidfileFlag := fl.String("pidfile")
212 pingbackFlag := fl.String("pingback")
213
214 // load all additional envs as soon as possible
215 err := handleEnvFileFlag(fl)
216 if err != nil {
217 return caddy.ExitCodeFailedStartup, err
218 }
219
220 // if we are supposed to print the environment, do that first
221 if printEnvFlag {
222 printEnvironment()
223 }
224
225 // load the config, depending on flags
226 var config []byte
227 if resumeFlag {
228 config, err = os.ReadFile(caddy.ConfigAutosavePath)
229 if errors.Is(err, fs.ErrNotExist) {
230 // not a bad error; just can't resume if autosave file doesn't exist
231 logger.Info("no autosave file exists", zap.String("autosave_file", caddy.ConfigAutosavePath))
232 resumeFlag = false
233 } else if err != nil {
234 return caddy.ExitCodeFailedStartup, err
235 } else {
236 if configFlag == "" {
237 logger.Info("resuming from last configuration",
238 zap.String("autosave_file", caddy.ConfigAutosavePath))
239 } else {
240 // if they also specified a config file, user should be aware that we're not
241 // using it (doing so could lead to data/config loss by overwriting!)
242 logger.Warn("--config and --resume flags were used together; ignoring --config and resuming from last configuration",
243 zap.String("autosave_file", caddy.ConfigAutosavePath))
244 }
245 }
246 }
247 // we don't use 'else' here since this value might have been changed in 'if' block; i.e. not mutually exclusive
248 var configFile string
249 var adapterUsed string
250 if !resumeFlag {
251 config, configFile, adapterUsed, err = LoadConfig(configFlag, configAdapterFlag)
252 if err != nil {
253 return caddy.ExitCodeFailedStartup, err
254 }
255 }
256
257 // create pidfile now, in case loading config takes a while (issue #5477)
258 if pidfileFlag != "" {
259 err := caddy.PIDFile(pidfileFlag)
260 if err != nil {
261 logger.Error("unable to write PID file",
262 zap.String("pidfile", pidfileFlag),
263 zap.Error(err))
264 }
265 }
266
267 // If we have a source config file (we're running via 'caddy run --config ...'),
268 // record it so SIGUSR1 can reload from the same file. Also provide a callback
269 // that knows how to load/adapt that source when requested by the main process.
270 if configFile != "" {
271 caddy.SetLastConfig(configFile, adapterUsed, func(file, adapter string) error {
272 cfg, _, _, err := LoadConfig(file, adapter)
273 if err != nil {
274 return err
275 }
276 return caddy.Load(cfg, true)
277 })
278 }
279
280 // run the initial config
281 err = caddy.Load(config, true)
282 if err != nil {
283 return caddy.ExitCodeFailedStartup, fmt.Errorf("loading initial config: %v", err)
284 }
285 // release the reference to the config so it can be GC'd
286 config = nil //nolint:ineffassign,wastedassign
287
288 // at this stage the config will have replaced the
289 // default logger to the configured one, so we can
290 // log normally, now that the config is running.
291 // also clear our ref to the buffer so it can get GC'd
292 logger = caddy.Log()
293 defaultLogger = nil //nolint:ineffassign,wastedassign
294 logger.Info("serving initial configuration")
295
296 // if we are to report to another process the successful start
297 // of the server, do so now by echoing back contents of stdin
298 if pingbackFlag != "" {
299 confirmationBytes, err := io.ReadAll(os.Stdin)
300 if err != nil {
301 return caddy.ExitCodeFailedStartup,
302 fmt.Errorf("reading confirmation bytes from stdin: %v", err)
303 }
304 conn, err := net.Dial("tcp", pingbackFlag)
305 if err != nil {
306 return caddy.ExitCodeFailedStartup,
307 fmt.Errorf("dialing confirmation address: %v", err)
308 }
309 _, err = conn.Write(confirmationBytes)
310 if err != nil {
311 return caddy.ExitCodeFailedStartup,
312 fmt.Errorf("writing confirmation bytes to %s: %v", pingbackFlag, err)
313 }
314 // close (non-defer because we `select {}` below)
315 // and release references so they can be GC'd
316 conn.Close()
317 confirmationBytes = nil //nolint:ineffassign,wastedassign
318 conn = nil //nolint:wastedassign,ineffassign
319 }
320
321 // if enabled, reload config file automatically on changes
322 // (this better only be used in dev!)
323 if watchFlag {
324 go watchConfigFile(configFile, adapterUsed)
325 }
326
327 // warn if the environment does not provide enough information about the disk
328 hasXDG := os.Getenv("XDG_DATA_HOME") != "" &&
329 os.Getenv("XDG_CONFIG_HOME") != "" &&
330 os.Getenv("XDG_CACHE_HOME") != ""
331 switch runtime.GOOS {
332 case "windows":
333 if os.Getenv("HOME") == "" && os.Getenv("USERPROFILE") == "" && !hasXDG {
334 logger.Warn("neither HOME nor USERPROFILE environment variables are set - please fix; some assets might be stored in ./caddy")
335 }
336 case "plan9":
337 if os.Getenv("home") == "" && !hasXDG {
338 logger.Warn("$home environment variable is empty - please fix; some assets might be stored in ./caddy")
339 }
340 default:
341 if os.Getenv("HOME") == "" && !hasXDG {
342 logger.Warn("$HOME environment variable is empty - please fix; some assets might be stored in ./caddy")
343 }
344 }
345
346 // release the last local logger reference
347 logger = nil //nolint:wastedassign,ineffassign
348
349 select {}
350 }
351
352 func cmdStop(fl Flags) (int, error) {
353 addressFlag := fl.String("address")
354 configFlag := fl.String("config")
355 configAdapterFlag := fl.String("adapter")
356
357 adminAddr, err := DetermineAdminAPIAddress(addressFlag, nil, configFlag, configAdapterFlag)
358 if err != nil {
359 return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
360 }
361
362 resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/stop", nil, nil)
363 if err != nil {
364 caddy.Log().Warn("failed using API to stop instance", zap.Error(err))
365 return caddy.ExitCodeFailedStartup, err
366 }
367 defer resp.Body.Close()
368
369 return caddy.ExitCodeSuccess, nil
370 }
371
372 func cmdReload(fl Flags) (int, error) {
373 configFlag := fl.String("config")
374 configAdapterFlag := fl.String("adapter")
375 addressFlag := fl.String("address")
376 forceFlag := fl.Bool("force")
377
378 // get the config in caddy's native format
379 config, configFile, adapterUsed, err := LoadConfig(configFlag, configAdapterFlag)
380 if err != nil {
381 return caddy.ExitCodeFailedStartup, err
382 }
383 if configFile == "" {
384 return caddy.ExitCodeFailedStartup, fmt.Errorf("no config file to load")
385 }
386
387 adminAddr, err := DetermineAdminAPIAddress(addressFlag, config, configFile, configAdapterFlag)
388 if err != nil {
389 return caddy.ExitCodeFailedStartup, fmt.Errorf("couldn't determine admin API address: %v", err)
390 }
391
392 // optionally force a config reload
393 headers := make(http.Header)
394 if forceFlag {
395 headers.Set("Cache-Control", "must-revalidate")
396 }
397 // Provide the source file/adapter to the running process so it can
398 // preserve its last-config knowledge if this reload came from the same source.
399 headers.Set("Caddy-Config-Source-File", configFile)
400 headers.Set("Caddy-Config-Source-Adapter", adapterUsed)
401
402 resp, err := AdminAPIRequest(adminAddr, http.MethodPost, "/load", headers, bytes.NewReader(config))
403 if err != nil {
404 return caddy.ExitCodeFailedStartup, fmt.Errorf("sending configuration to instance: %v", err)
405 }
406 defer resp.Body.Close()
407
408 return caddy.ExitCodeSuccess, nil
409 }
410
411 func cmdVersion(_ Flags) (int, error) {
412 _, full := caddy.Version()
413 fmt.Println(full)
414 return caddy.ExitCodeSuccess, nil
415 }
416
417 func cmdBuildInfo(_ Flags) (int, error) {
418 bi, ok := debug.ReadBuildInfo()
419 if !ok {
420 return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
421 }
422 fmt.Println(bi)
423 return caddy.ExitCodeSuccess, nil
424 }
425
426 // jsonModuleInfo holds metadata about a Caddy module for JSON output.
427 type jsonModuleInfo struct {
428 ModuleName string `json:"module_name"`
429 ModuleType string `json:"module_type"`
430 Version string `json:"version,omitempty"`
431 PackageURL string `json:"package_url,omitempty"`
432 }
433
434 func cmdListModules(fl Flags) (int, error) {
435 packages := fl.Bool("packages")
436 versions := fl.Bool("versions")
437 skipStandard := fl.Bool("skip-standard")
438 jsonOutput := fl.Bool("json")
439
440 // Organize modules by whether they come with the standard distribution
441 standard, nonstandard, unknown, err := getModules()
442 if err != nil {
443 // If module info can't be fetched, just print the IDs and exit
444 for _, m := range caddy.Modules() {
445 fmt.Println(m)
446 }
447 return caddy.ExitCodeSuccess, nil
448 }
449
450 // Logic for JSON output
451 if jsonOutput {
452 output := []jsonModuleInfo{}
453
454 // addToOutput is a helper to convert internal module info to the JSON-serializable struct
455 addToOutput := func(list []moduleInfo, moduleType string) {
456 for _, mi := range list {
457 item := jsonModuleInfo{
458 ModuleName: mi.caddyModuleID,
459 ModuleType: moduleType, // Mapping the type here
460 }
461 if mi.goModule != nil {
462 item.Version = mi.goModule.Version
463 item.PackageURL = mi.goModule.Path
464 }
465 output = append(output, item)
466 }
467 }
468
469 // Pass the respective type for each category
470 if !skipStandard {
471 addToOutput(standard, "standard")
472 }
473 addToOutput(nonstandard, "non-standard")
474 addToOutput(unknown, "unknown")
475
476 jsonBytes, err := json.MarshalIndent(output, "", " ")
477 if err != nil {
478 return caddy.ExitCodeFailedQuit, err
479 }
480 fmt.Println(string(jsonBytes))
481 return caddy.ExitCodeSuccess, nil
482 }
483
484 // Logic for Text output (Fallback)
485 printModuleInfo := func(mi moduleInfo) {
486 fmt.Print(mi.caddyModuleID)
487 if versions && mi.goModule != nil {
488 fmt.Print(" " + mi.goModule.Version)
489 }
490 if packages && mi.goModule != nil {
491 fmt.Print(" " + mi.goModule.Path)
492 if mi.goModule.Replace != nil {
493 fmt.Print(" => " + mi.goModule.Replace.Path)
494 }
495 }
496 if mi.err != nil {
497 fmt.Printf(" [%v]", mi.err)
498 }
499 fmt.Println()
500 }
501
502 // Standard modules (always shipped with Caddy)
503 if !skipStandard {
504 if len(standard) > 0 {
505 for _, mod := range standard {
506 printModuleInfo(mod)
507 }
508 }
509 fmt.Printf("\n Standard modules: %d\n", len(standard))
510 }
511
512 // Non-standard modules (third party plugins)
513 if len(nonstandard) > 0 {
514 if len(standard) > 0 && !skipStandard {
515 fmt.Println()
516 }
517 for _, mod := range nonstandard {
518 printModuleInfo(mod)
519 }
520 fmt.Printf("\n Non-standard modules: %d\n", len(nonstandard))
521 }
522
523 // Unknown modules (couldn't get Caddy module info)
524 if len(unknown) > 0 {
525 if (len(standard) > 0 && !skipStandard) || len(nonstandard) > 0 {
526 fmt.Println()
527 }
528 for _, mod := range unknown {
529 printModuleInfo(mod)
530 }
531 fmt.Printf("\n Unknown modules: %d\n", len(unknown))
532 }
533
534 return caddy.ExitCodeSuccess, nil
535 }
536
537 func cmdEnviron(fl Flags) (int, error) {
538 // load all additional envs as soon as possible
539 err := handleEnvFileFlag(fl)
540 if err != nil {
541 return caddy.ExitCodeFailedStartup, err
542 }
543
544 printEnvironment()
545 return caddy.ExitCodeSuccess, nil
546 }
547
548 func cmdAdaptConfig(fl Flags) (int, error) {
549 configFlag := fl.String("config")
550 adapterFlag := fl.String("adapter")
551 prettyFlag := fl.Bool("pretty")
552 validateFlag := fl.Bool("validate")
553
554 var err error
555 configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
556 if err != nil {
557 return caddy.ExitCodeFailedStartup, err
558 }
559 if configFlag == "" {
560 return caddy.ExitCodeFailedStartup,
561 fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
562 }
563
564 // load all additional envs as soon as possible
565 err = handleEnvFileFlag(fl)
566 if err != nil {
567 return caddy.ExitCodeFailedStartup, err
568 }
569
570 if adapterFlag == "" {
571 return caddy.ExitCodeFailedStartup,
572 fmt.Errorf("adapter name is required (use --adapt flag or leave unspecified for default)")
573 }
574
575 cfgAdapter := caddyconfig.GetAdapter(adapterFlag)
576 if cfgAdapter == nil {
577 return caddy.ExitCodeFailedStartup,
578 fmt.Errorf("unrecognized config adapter: %s", adapterFlag)
579 }
580
581 var input []byte
582 // read from stdin if the file name is "-"
583 if configFlag == "-" {
584 input, err = io.ReadAll(os.Stdin)
585 } else {
586 input, err = os.ReadFile(configFlag)
587 }
588 if err != nil {
589 return caddy.ExitCodeFailedStartup,
590 fmt.Errorf("reading input file: %v", err)
591 }
592
593 opts := map[string]any{"filename": configFlag}
594
595 adaptedConfig, warnings, err := cfgAdapter.Adapt(input, opts)
596 if err != nil {
597 return caddy.ExitCodeFailedStartup, err
598 }
599
600 if prettyFlag {
601 var prettyBuf bytes.Buffer
602 err = json.Indent(&prettyBuf, adaptedConfig, "", "\t")
603 if err != nil {
604 return caddy.ExitCodeFailedStartup, err
605 }
606 adaptedConfig = prettyBuf.Bytes()
607 }
608
609 // print result to stdout
610 fmt.Println(string(adaptedConfig))
611
612 // print warnings to stderr
613 for _, warn := range warnings {
614 msg := warn.Message
615 if warn.Directive != "" {
616 msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
617 }
618 caddy.Log().Named(adapterFlag).Warn(msg,
619 zap.String("file", warn.File),
620 zap.Int("line", warn.Line))
621 }
622
623 // validate output if requested
624 if validateFlag {
625 var cfg *caddy.Config
626 err = caddy.StrictUnmarshalJSON(adaptedConfig, &cfg)
627 if err != nil {
628 return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
629 }
630 err = caddy.Validate(cfg)
631 if err != nil {
632 return caddy.ExitCodeFailedStartup, fmt.Errorf("validation: %v", err)
633 }
634 }
635
636 return caddy.ExitCodeSuccess, nil
637 }
638
639 func cmdValidateConfig(fl Flags) (int, error) {
640 configFlag := fl.String("config")
641 adapterFlag := fl.String("adapter")
642
643 // load all additional envs as soon as possible
644 err := handleEnvFileFlag(fl)
645 if err != nil {
646 return caddy.ExitCodeFailedStartup, err
647 }
648
649 // use default config and ensure a config file is specified
650 configFlag, err = configFileWithRespectToDefault(caddy.Log(), configFlag)
651 if err != nil {
652 return caddy.ExitCodeFailedStartup, err
653 }
654 if configFlag == "" {
655 return caddy.ExitCodeFailedStartup,
656 fmt.Errorf("input file required when there is no Caddyfile in current directory (use --config flag)")
657 }
658
659 input, _, _, err := LoadConfig(configFlag, adapterFlag)
660 if err != nil {
661 return caddy.ExitCodeFailedStartup, err
662 }
663 input = caddy.RemoveMetaFields(input)
664
665 var cfg *caddy.Config
666 err = caddy.StrictUnmarshalJSON(input, &cfg)
667 if err != nil {
668 return caddy.ExitCodeFailedStartup, fmt.Errorf("decoding config: %v", err)
669 }
670
671 err = caddy.Validate(cfg)
672 if err != nil {
673 return caddy.ExitCodeFailedStartup, err
674 }
675
676 fmt.Println("Valid configuration")
677
678 return caddy.ExitCodeSuccess, nil
679 }
680
681 func cmdFmt(fl Flags) (int, error) {
682 configFile := fl.Arg(0)
683 configFlag := fl.String("config")
684 if (len(fl.Args()) > 1) || (configFlag != "" && configFile != "") {
685 return caddy.ExitCodeFailedStartup, fmt.Errorf("fmt does not support multiple files %s %s", configFlag, strings.Join(fl.Args(), " "))
686 }
687 if configFile == "" && configFlag == "" {
688 configFile = "Caddyfile"
689 } else if configFile == "" {
690 configFile = configFlag
691 }
692
693 useImports := fl.Bool("imports")
694
695 // --imports cannot be used with stdin: there is no source directory to
696 // resolve import paths against.
697 if useImports && configFile == "-" {
698 return caddy.ExitCodeFailedStartup,
699 fmt.Errorf("cannot use --imports when reading from stdin (no source directory to resolve imports against)")
700 }
701
702 // as a special case, read from stdin if the file name is "-"
703 if configFile == "-" {
704 input, err := io.ReadAll(os.Stdin)
705 if err != nil {
706 return caddy.ExitCodeFailedStartup,
707 fmt.Errorf("reading stdin: %v", err)
708 }
709 fmt.Print(string(caddyfile.Format(input)))
710 return caddy.ExitCodeSuccess, nil
711 }
712
713 // --imports: format the root file and all files it references via import
714 // directives, then write or print each one.
715 if useImports {
716 results, err := caddyfile.FormatImports(configFile, caddyfile.FormatOptions{})
717 if err != nil {
718 return caddy.ExitCodeFailedStartup, fmt.Errorf("formatting imports: %v", err)
719 }
720
721 if fl.Bool("overwrite") {
722 if err := overwriteFormattedFiles(results); err != nil {
723 return caddy.ExitCodeFailedStartup, err
724 }
725 return caddy.ExitCodeSuccess, nil
726 }
727
728 originals := make([][]byte, len(results))
729 var formattingWarning caddyconfig.Warning
730 for i, f := range results {
731 original, readErr := os.ReadFile(f.Path)
732 if readErr != nil {
733 return caddy.ExitCodeFailedStartup, fmt.Errorf("reading file for formatting comparison %s: %v", f.Path, readErr)
734 }
735 originals[i] = original
736 if warning, different := caddyfile.FormattingDifference(f.Path, original); different && formattingWarning.File == "" {
737 formattingWarning = warning
738 }
739 }
740
741 for i, f := range results {
742 fmt.Printf("# %s\n", f.Path)
743 if fl.Bool("diff") {
744 diff := difflib.Diff(
745 strings.Split(string(originals[i]), "\n"),
746 strings.Split(string(f.Content), "\n"))
747 for _, d := range diff {
748 switch d.Delta {
749 case difflib.Common:
750 fmt.Printf(" %s\n", d.Payload)
751 case difflib.LeftOnly:
752 fmt.Printf("- %s\n", d.Payload)
753 case difflib.RightOnly:
754 fmt.Printf("+ %s\n", d.Payload)
755 }
756 }
757 } else {
758 fmt.Print(string(f.Content))
759 }
760 }
761 if formattingWarning.File != "" {
762 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`,
763 formattingWarning.File,
764 formattingWarning.Line,
765 )
766 }
767 return caddy.ExitCodeSuccess, nil
768 }
769
770 input, err := os.ReadFile(configFile)
771 if err != nil {
772 return caddy.ExitCodeFailedStartup,
773 fmt.Errorf("reading input file: %v", err)
774 }
775
776 output := caddyfile.Format(input)
777
778 if fl.Bool("overwrite") {
779 if err := os.WriteFile(configFile, output, 0o600); err != nil { //nolint:gosec // path traversal is not really a thing here, this is either "Caddyfile" or admin-controlled
780 return caddy.ExitCodeFailedStartup, fmt.Errorf("overwriting formatted file: %v", err)
781 }
782 return caddy.ExitCodeSuccess, nil
783 }
784
785 if fl.Bool("diff") {
786 diff := difflib.Diff(
787 strings.Split(string(input), "\n"),
788 strings.Split(string(output), "\n"))
789 for _, d := range diff {
790 switch d.Delta {
791 case difflib.Common:
792 fmt.Printf(" %s\n", d.Payload)
793 case difflib.LeftOnly:
794 fmt.Printf("- %s\n", d.Payload)
795 case difflib.RightOnly:
796 fmt.Printf("+ %s\n", d.Payload)
797 }
798 }
799 } else {
800 fmt.Print(string(output))
801 }
802
803 if warning, diff := caddyfile.FormattingDifference(configFile, input); diff {
804 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`,
805 warning.File,
806 warning.Line,
807 )
808 }
809
810 return caddy.ExitCodeSuccess, nil
811 }
812
813 type formattedFileOverwrite struct {
814 path string
815 content []byte
816 original []byte
817 info fs.FileInfo
818 }
819
820 func overwriteFormattedFiles(results []caddyfile.FormattedFile) error {
821 files := make([]formattedFileOverwrite, 0, len(results))
822 for _, result := range results {
823 path, err := filepath.EvalSymlinks(filepath.Dir(result.Path))
824 if err != nil {
825 return fmt.Errorf("preparing formatted file %s: resolving parent directory: %v", result.Path, err)
826 }
827 path = filepath.Join(path, filepath.Base(result.Path))
828
829 file, err := os.Open(path)
830 if err != nil {
831 return fmt.Errorf("preparing formatted file %s: %v", result.Path, err)
832 }
833 info, statErr := file.Stat()
834 pathInfo, lstatErr := os.Lstat(path)
835 original, readErr := io.ReadAll(file)
836 closeErr := file.Close()
837 if statErr != nil {
838 return fmt.Errorf("preparing formatted file %s: stating open file: %v", result.Path, statErr)
839 }
840 if lstatErr != nil {
841 return fmt.Errorf("preparing formatted file %s: checking destination: %v", result.Path, lstatErr)
842 }
843 if readErr != nil {
844 return fmt.Errorf("preparing formatted file %s: reading destination: %v", result.Path, readErr)
845 }
846 if closeErr != nil {
847 return fmt.Errorf("preparing formatted file %s: %v", result.Path, closeErr)
848 }
849 if pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.Mode().IsRegular() || !os.SameFile(info, pathInfo) {
850 return fmt.Errorf("refusing to overwrite %s: destination is not the regular file that was opened", result.Path)
851 }
852 if !bytes.Equal(caddyfile.FormatWithOptions(original, caddyfile.FormatOptions{}), result.Content) {
853 return fmt.Errorf("refusing to overwrite %s: file changed after it was formatted", result.Path)
854 }
855 files = append(files, formattedFileOverwrite{path: path, content: result.Content, original: original, info: info})
856 }
857
858 for i, file := range files {
859 if err := atomicReplaceFormattedFile(file); err != nil {
860 if i > 0 {
861 return fmt.Errorf("overwriting formatted file %s after replacing %d of %d files: %v; configuration files may be partially updated", file.path, i, len(files), err)
862 }
863 return fmt.Errorf("overwriting formatted file %s before replacing any files: %v", file.path, err)
864 }
865 }
866 return nil
867 }
868
869 func atomicReplaceFormattedFile(file formattedFileOverwrite) (err error) {
870 temp, err := os.CreateTemp(filepath.Dir(file.path), ".caddy-fmt-*")
871 if err != nil {
872 return err
873 }
874 tempPath := temp.Name()
875 defer func() {
876 _ = temp.Close()
877 if err != nil {
878 _ = os.Remove(tempPath)
879 }
880 }()
881
882 if _, err = temp.Write(file.content); err != nil {
883 return err
884 }
885 if err = temp.Chmod(file.info.Mode().Perm()); err != nil {
886 return err
887 }
888 if err = temp.Sync(); err != nil {
889 return err
890 }
891 if err = temp.Close(); err != nil {
892 return err
893 }
894
895 destination, err := os.Open(file.path)
896 if err != nil {
897 return fmt.Errorf("reopening destination: %v", err)
898 }
899 destinationInfo, statErr := destination.Stat()
900 pathInfo, lstatErr := os.Lstat(file.path)
901 current, readErr := io.ReadAll(destination)
902 closeErr := destination.Close()
903 if statErr != nil {
904 return fmt.Errorf("restating destination: %v", statErr)
905 }
906 if lstatErr != nil {
907 return fmt.Errorf("rechecking destination: %v", lstatErr)
908 }
909 if readErr != nil {
910 return fmt.Errorf("rereading destination: %v", readErr)
911 }
912 if closeErr != nil {
913 return fmt.Errorf("closing destination: %v", closeErr)
914 }
915 if pathInfo.Mode()&os.ModeSymlink != 0 || !pathInfo.Mode().IsRegular() ||
916 !os.SameFile(file.info, destinationInfo) || !os.SameFile(destinationInfo, pathInfo) ||
917 !bytes.Equal(current, file.original) {
918 return fmt.Errorf("destination changed while formatting")
919 }
920 if err = os.Rename(tempPath, file.path); err != nil {
921 return err
922 }
923 return nil
924 }
925
926 // handleEnvFileFlag loads the environment variables from the given --envfile
927 // flag if specified. This should be called as early in the command function.
928 func handleEnvFileFlag(fl Flags) error {
929 var err error
930 var envfileFlag []string
931 envfileFlag, err = fl.GetStringSlice("envfile")
932 if err != nil {
933 return fmt.Errorf("reading envfile flag: %v", err)
934 }
935
936 for _, envfile := range envfileFlag {
937 if err := loadEnvFromFile(envfile); err != nil {
938 return fmt.Errorf("loading additional environment variables: %v", err)
939 }
940 }
941
942 return nil
943 }
944
945 // AdminAPIRequest makes an API request according to the CLI flags given,
946 // with the given HTTP method and request URI. If body is non-nil, it will
947 // be assumed to be Content-Type application/json. The caller should close
948 // the response body. Should only be used by Caddy CLI commands which
949 // need to interact with a running instance of Caddy via the admin API.
950 func AdminAPIRequest(adminAddr, method, uri string, headers http.Header, body io.Reader) (*http.Response, error) {
951 parsedAddr, err := caddy.ParseNetworkAddress(adminAddr)
952 if err != nil || parsedAddr.PortRangeSize() > 1 {
953 return nil, fmt.Errorf("invalid admin address %s: %v", adminAddr, err)
954 }
955 origin := "http://" + parsedAddr.JoinHostPort(0)
956 if parsedAddr.IsUnixNetwork() {
957 origin = "http://127.0.0.1" // bogus host is a hack so that http.NewRequest() is happy
958
959 // the unix address at this point might still contain the optional
960 // unix socket permissions, which are part of the address/host.
961 // those need to be removed first, as they aren't part of the
962 // resulting unix file path
963 addr, _, err := internal.SplitUnixSocketPermissionsBits(parsedAddr.Host)
964 if err != nil {
965 return nil, err
966 }
967 parsedAddr.Host = addr
968 } else if parsedAddr.IsFdNetwork() {
969 origin = "http://127.0.0.1"
970 }
971
972 // form the request
973 req, err := http.NewRequest(method, origin+uri, body)
974 if err != nil {
975 return nil, fmt.Errorf("making request: %v", err)
976 }
977 if parsedAddr.IsUnixNetwork() || parsedAddr.IsFdNetwork() {
978 // We used to conform to RFC 2616 Section 14.26 which requires
979 // an empty host header when there is no host, as is the case
980 // with unix sockets and socket fds. However, Go required a
981 // Host value so we used a hack of a space character as the host
982 // (it would see the Host was non-empty, then trim the space later).
983 // As of Go 1.20.6 (July 2023), this hack no longer works. See:
984 // https://github.com/golang/go/issues/60374
985 // See also the discussion here:
986 // https://github.com/golang/go/issues/61431
987 //
988 // After that, we now require a Host value of either 127.0.0.1
989 // or ::1 if one is set. Above I choose to use 127.0.0.1. Even
990 // though the value should be completely irrelevant (it could be
991 // "srldkjfsd"), if for some reason the Host *is* used, at least
992 // we can have some reasonable assurance it will stay on the local
993 // machine and that browsers, if they ever allow access to unix
994 // sockets, can still enforce CORS, ensuring it is still coming
995 // from the local machine.
996 } else {
997 req.Header.Set("Origin", origin)
998 }
999 if body != nil {
1000 req.Header.Set("Content-Type", "application/json")
1001 }
1002 maps.Copy(req.Header, headers)
1003
1004 // make an HTTP client that dials our network type, since admin
1005 // endpoints aren't always TCP, which is what the default transport
1006 // expects; reuse is not of particular concern here
1007 client := http.Client{
1008 Transport: &http.Transport{
1009 DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
1010 return net.Dial(parsedAddr.Network, parsedAddr.JoinHostPort(0))
1011 },
1012 },
1013 }
1014
1015 resp, err := client.Do(req) //nolint:gosec // the only SSRF here would be self-sabotage I think
1016 if err != nil {
1017 return nil, fmt.Errorf("performing request: %v", err)
1018 }
1019
1020 // if it didn't work, let the user know
1021 if resp.StatusCode >= 400 {
1022 respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*2))
1023 if err != nil {
1024 return nil, fmt.Errorf("HTTP %d: reading error message: %v", resp.StatusCode, err)
1025 }
1026 return nil, fmt.Errorf("caddy responded with error: HTTP %d: %s", resp.StatusCode, respBody)
1027 }
1028
1029 return resp, nil
1030 }
1031
1032 // DetermineAdminAPIAddress determines which admin API endpoint address should
1033 // be used based on the inputs. By priority: if `address` is specified, then
1034 // it is returned; if `config` is specified, then that config will be used for
1035 // finding the admin address; if `configFile` (and `configAdapter`) are specified,
1036 // then that config will be loaded to find the admin address; otherwise, the
1037 // default admin listen address will be returned.
1038 func DetermineAdminAPIAddress(address string, config []byte, configFile, configAdapter string) (string, error) {
1039 // Prefer the address if specified and non-empty
1040 if address != "" {
1041 return address, nil
1042 }
1043
1044 // Try to load the config from file if specified, with the given adapter name
1045 if configFile != "" {
1046 var loadedConfigFile string
1047 var err error
1048
1049 // use the provided loaded config if non-empty
1050 // otherwise, load it from the specified file/adapter
1051 loadedConfig := config
1052 if len(loadedConfig) == 0 {
1053 // get the config in caddy's native format
1054 loadedConfig, loadedConfigFile, _, err = LoadConfig(configFile, configAdapter)
1055 if err != nil {
1056 return "", err
1057 }
1058 if loadedConfigFile == "" {
1059 return "", fmt.Errorf("no config file to load; either use --config flag or ensure Caddyfile exists in current directory")
1060 }
1061 }
1062
1063 // get the address of the admin listener from the config
1064 if len(loadedConfig) > 0 {
1065 var tmpStruct struct {
1066 Admin caddy.AdminConfig `json:"admin"`
1067 }
1068 err := json.Unmarshal(loadedConfig, &tmpStruct)
1069 if err != nil {
1070 return "", fmt.Errorf("unmarshaling admin listener address from config: %v", err)
1071 }
1072 if tmpStruct.Admin.Listen != "" {
1073 return tmpStruct.Admin.Listen, nil
1074 }
1075 }
1076 }
1077
1078 // Fallback to the default listen address otherwise
1079 return caddy.DefaultAdminListen, nil
1080 }
1081
1082 // configFileWithRespectToDefault returns the filename to use for loading the config, based
1083 // on whether a config file is already specified and a supported default config file exists.
1084 func configFileWithRespectToDefault(logger *zap.Logger, configFile string) (string, error) {
1085 const defaultCaddyfile = "Caddyfile"
1086
1087 // if no input file was specified, try a default Caddyfile if the Caddyfile adapter is plugged in
1088 if configFile == "" && caddyconfig.GetAdapter("caddyfile") != nil {
1089 _, err := os.Stat(defaultCaddyfile)
1090 if err == nil {
1091 // default Caddyfile exists
1092 if logger != nil {
1093 logger.Info("using adjacent Caddyfile")
1094 }
1095 return defaultCaddyfile, nil
1096 }
1097 if !errors.Is(err, fs.ErrNotExist) {
1098 // problem checking
1099 return configFile, fmt.Errorf("checking if default Caddyfile exists: %v", err)
1100 }
1101 }
1102
1103 // default config file does not exist or is irrelevant
1104 return configFile, nil
1105 }
1106
1107 type moduleInfo struct {
1108 caddyModuleID string
1109 goModule *debug.Module
1110 err error
1111 }