@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 516 lines 15.5 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 "bufio"
19 "bytes"
20 "encoding/json"
21 "errors"
22 "flag"
23 "fmt"
24 "io"
25 "io/fs"
26 "log"
27 "log/slog"
28 "net"
29 "os"
30 "path/filepath"
31 "runtime"
32 "runtime/debug"
33 "strconv"
34 "strings"
35 "time"
36
37 "github.com/KimMachineGun/automemlimit/memlimit"
38 "github.com/caddyserver/certmagic"
39 "github.com/spf13/pflag"
40 "go.uber.org/automaxprocs/maxprocs"
41 "go.uber.org/zap"
42 "go.uber.org/zap/exp/zapslog"
43
44 "github.com/caddyserver/caddy/v2"
45 "github.com/caddyserver/caddy/v2/caddyconfig"
46 )
47
48 func init() {
49 // set a fitting User-Agent for ACME requests
50 version, _ := caddy.Version()
51 cleanModVersion := strings.TrimPrefix(version, "v")
52 ua := "Caddy/" + cleanModVersion
53 if uaEnv, ok := os.LookupEnv("USERAGENT"); ok {
54 ua = uaEnv + " " + ua
55 }
56 certmagic.UserAgent = ua
57
58 // by using Caddy, user indicates agreement to CA terms
59 // (very important, as Caddy is often non-interactive
60 // and thus ACME account creation will fail!)
61 certmagic.DefaultACME.Agreed = true
62 }
63
64 // Main implements the main function of the caddy command.
65 // Call this if Caddy is to be the main() of your program.
66 func Main() {
67 if len(os.Args) == 0 {
68 fmt.Printf("[FATAL] no arguments provided by OS; args[0] must be command\n")
69 os.Exit(caddy.ExitCodeFailedStartup)
70 }
71
72 if err := defaultFactory.Build().Execute(); err != nil {
73 if exitErr, ok := errors.AsType[*exitError](err); ok {
74 os.Exit(exitErr.ExitCode)
75 }
76 os.Exit(1)
77 }
78 }
79
80 // handlePingbackConn reads from conn and ensures it matches
81 // the bytes in expect, or returns an error if it doesn't.
82 func handlePingbackConn(conn net.Conn, expect []byte) error {
83 defer conn.Close()
84 confirmationBytes, err := io.ReadAll(io.LimitReader(conn, 32))
85 if err != nil {
86 return err
87 }
88 if !bytes.Equal(confirmationBytes, expect) {
89 return fmt.Errorf("wrong confirmation: %x", confirmationBytes)
90 }
91 return nil
92 }
93
94 // LoadConfig loads the config from configFile and adapts it
95 // using adapterName. If adapterName is specified, configFile
96 // must be also. If no configFile is specified, it tries
97 // loading a default config file. The lack of a config file is
98 // not treated as an error, but false will be returned if
99 // there is no config available. It prints any warnings to stderr,
100 // and returns the resulting JSON config bytes along with
101 // the name of the loaded config file (if any).
102 // The return values are:
103 // - config bytes (nil if no config)
104 // - config file used ("" if none)
105 // - adapter used ("" if none)
106 // - error, if any
107 func LoadConfig(configFile, adapterName string) ([]byte, string, string, error) {
108 return loadConfigWithLogger(caddy.Log(), configFile, adapterName)
109 }
110
111 func isCaddyfile(configFile, adapterName string) (bool, error) {
112 if adapterName == "caddyfile" {
113 return true, nil
114 }
115
116 // as a special case, if a config file starts with "caddyfile" or
117 // has a ".caddyfile" extension, and no adapter is specified, and
118 // no adapter module name matches the extension, assume
119 // caddyfile adapter for convenience
120 baseConfig := strings.ToLower(filepath.Base(configFile))
121 baseConfigExt := filepath.Ext(baseConfig)
122 startsOrEndsInCaddyfile := strings.HasPrefix(baseConfig, "caddyfile") || strings.HasSuffix(baseConfig, ".caddyfile")
123
124 if baseConfigExt == ".json" {
125 return false, nil
126 }
127
128 // If the adapter is not specified,
129 // the config file starts with "caddyfile",
130 // the config file has an extension,
131 // and isn't a JSON file (e.g. Caddyfile.yaml),
132 // then we don't know what the config format is.
133 if adapterName == "" && startsOrEndsInCaddyfile {
134 return true, nil
135 }
136
137 // adapter is not empty,
138 // adapter is not "caddyfile",
139 // extension is not ".json",
140 // extension is not ".caddyfile"
141 // file does not start with "Caddyfile"
142 return false, nil
143 }
144
145 func loadConfigWithLogger(logger *zap.Logger, configFile, adapterName string) ([]byte, string, string, error) {
146 // if no logger is provided, use a nop logger
147 // just so we don't have to check for nil
148 if logger == nil {
149 logger = zap.NewNop()
150 }
151
152 // specifying an adapter without a config file is ambiguous
153 if adapterName != "" && configFile == "" {
154 return nil, "", "", fmt.Errorf("cannot adapt config without config file (use --config)")
155 }
156
157 // load initial config and adapter
158 var config []byte
159 var cfgAdapter caddyconfig.Adapter
160 var err error
161 if configFile != "" {
162 if configFile == "-" {
163 config, err = io.ReadAll(os.Stdin)
164 if err != nil {
165 return nil, "", "", fmt.Errorf("reading config from stdin: %v", err)
166 }
167 logger.Info("using config from stdin")
168 } else {
169 config, err = os.ReadFile(configFile)
170 if err != nil {
171 return nil, "", "", fmt.Errorf("reading config from file: %v", err)
172 }
173 logger.Info("using config from file", zap.String("file", configFile))
174 }
175 } else if adapterName == "" {
176 // if the Caddyfile adapter is plugged in, we can try using an
177 // adjacent Caddyfile by default
178 cfgAdapter = caddyconfig.GetAdapter("caddyfile")
179 if cfgAdapter != nil {
180 config, err = os.ReadFile("Caddyfile")
181 if errors.Is(err, fs.ErrNotExist) {
182 // okay, no default Caddyfile; pretend like this never happened
183 cfgAdapter = nil
184 } else if err != nil {
185 // default Caddyfile exists, but error reading it
186 return nil, "", "", fmt.Errorf("reading default Caddyfile: %v", err)
187 } else {
188 // success reading default Caddyfile
189 configFile = "Caddyfile"
190 logger.Info("using adjacent Caddyfile")
191 }
192 }
193 }
194
195 if yes, err := isCaddyfile(configFile, adapterName); yes {
196 adapterName = "caddyfile"
197 } else if err != nil {
198 return nil, "", "", err
199 }
200
201 // load config adapter
202 if adapterName != "" {
203 cfgAdapter = caddyconfig.GetAdapter(adapterName)
204 if cfgAdapter == nil {
205 return nil, "", "", fmt.Errorf("unrecognized config adapter: %s", adapterName)
206 }
207 }
208
209 // adapt config
210 if cfgAdapter != nil {
211 adaptedConfig, warnings, err := cfgAdapter.Adapt(config, map[string]any{
212 "filename": configFile,
213 })
214 if err != nil {
215 return nil, "", "", fmt.Errorf("adapting config using %s: %v", adapterName, err)
216 }
217 logger.Info("adapted config to JSON", zap.String("adapter", adapterName))
218 for _, warn := range warnings {
219 msg := warn.Message
220 if warn.Directive != "" {
221 msg = fmt.Sprintf("%s: %s", warn.Directive, warn.Message)
222 }
223 logger.Warn(msg,
224 zap.String("adapter", adapterName),
225 zap.String("file", warn.File),
226 zap.Int("line", warn.Line))
227 }
228 config = adaptedConfig
229 } else if len(config) != 0 {
230 // validate that the config is at least valid JSON
231 err = json.Unmarshal(config, new(any))
232 if err != nil {
233 if jsonErr, ok := err.(*json.SyntaxError); ok {
234 return nil, "", "", fmt.Errorf("config is not valid JSON: %w, at offset %d; did you mean to use a config adapter (the --adapter flag)?", err, jsonErr.Offset)
235 }
236 return nil, "", "", fmt.Errorf("config is not valid JSON: %w; did you mean to use a config adapter (the --adapter flag)?", err)
237 }
238 }
239
240 return config, configFile, adapterName, nil
241 }
242
243 // watchConfigFile watches the config file at filename for changes
244 // and reloads the config if the file was updated. This function
245 // blocks indefinitely; it only quits if the poller has errors for
246 // long enough time. The filename passed in must be the actual
247 // config file used, not one to be discovered.
248 // Each second the config files is loaded and parsed into an object
249 // and is compared to the last config object that was loaded
250 func watchConfigFile(filename, adapterName string) {
251 defer func() {
252 if err := recover(); err != nil {
253 log.Printf("[PANIC] watching config file: %v\n%s", err, debug.Stack())
254 }
255 }()
256
257 // make our logger; since config reloads can change the
258 // default logger, we need to get it dynamically each time
259 logger := func() *zap.Logger {
260 return caddy.Log().
261 Named("watcher").
262 With(zap.String("config_file", filename))
263 }
264
265 // get current config
266 lastCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
267 if err != nil {
268 logger().Error("unable to load latest config", zap.Error(err))
269 return
270 }
271
272 logger().Info("watching config file for changes")
273
274 // begin poller
275 //nolint:staticcheck
276 for range time.Tick(1 * time.Second) {
277 // get current config
278 newCfg, _, _, err := loadConfigWithLogger(nil, filename, adapterName)
279 if err != nil {
280 logger().Error("unable to load latest config", zap.Error(err))
281 return
282 }
283
284 // if it hasn't changed, nothing to do
285 if bytes.Equal(lastCfg, newCfg) {
286 continue
287 }
288 logger().Info("config file changed; reloading")
289
290 // remember the current config
291 lastCfg = newCfg
292
293 // apply the updated config
294 err = caddy.Load(lastCfg, false)
295 if err != nil {
296 logger().Error("applying latest config", zap.Error(err))
297 continue
298 }
299 }
300 }
301
302 // Flags wraps a FlagSet so that typed values
303 // from flags can be easily retrieved.
304 type Flags struct {
305 *pflag.FlagSet
306 }
307
308 // String returns the string representation of the
309 // flag given by name. It panics if the flag is not
310 // in the flag set.
311 func (f Flags) String(name string) string {
312 return f.FlagSet.Lookup(name).Value.String()
313 }
314
315 // Bool returns the boolean representation of the
316 // flag given by name. It returns false if the flag
317 // is not a boolean type. It panics if the flag is
318 // not in the flag set.
319 func (f Flags) Bool(name string) bool {
320 val, _ := strconv.ParseBool(f.String(name))
321 return val
322 }
323
324 // Int returns the integer representation of the
325 // flag given by name. It returns 0 if the flag
326 // is not an integer type. It panics if the flag is
327 // not in the flag set.
328 func (f Flags) Int(name string) int {
329 val, _ := strconv.ParseInt(f.String(name), 0, strconv.IntSize)
330 return int(val)
331 }
332
333 // Float64 returns the float64 representation of the
334 // flag given by name. It returns false if the flag
335 // is not a float64 type. It panics if the flag is
336 // not in the flag set.
337 func (f Flags) Float64(name string) float64 {
338 val, _ := strconv.ParseFloat(f.String(name), 64)
339 return val
340 }
341
342 // Duration returns the duration representation of the
343 // flag given by name. It returns false if the flag
344 // is not a duration type. It panics if the flag is
345 // not in the flag set.
346 func (f Flags) Duration(name string) time.Duration {
347 val, _ := caddy.ParseDuration(f.String(name))
348 return val
349 }
350
351 func loadEnvFromFile(envFile string) error {
352 file, err := os.Open(envFile)
353 if err != nil {
354 return fmt.Errorf("reading environment file: %v", err)
355 }
356 defer file.Close()
357
358 envMap, err := parseEnvFile(file)
359 if err != nil {
360 return fmt.Errorf("parsing environment file: %v", err)
361 }
362
363 for k, v := range envMap {
364 // do not overwrite existing environment variables
365 _, exists := os.LookupEnv(k)
366 if !exists {
367 if err := os.Setenv(k, v); err != nil {
368 return fmt.Errorf("setting environment variables: %v", err)
369 }
370 }
371 }
372
373 // Update the storage paths to ensure they have the proper
374 // value after loading a specified env file.
375 caddy.ConfigAutosavePath = filepath.Join(caddy.AppConfigDir(), "autosave.json")
376 caddy.DefaultStorage = &certmagic.FileStorage{Path: caddy.AppDataDir()}
377
378 return nil
379 }
380
381 // parseEnvFile parses an env file from KEY=VALUE format.
382 // It's pretty naive. Limited value quotation is supported,
383 // but variable and command expansions are not supported.
384 func parseEnvFile(envInput io.Reader) (map[string]string, error) {
385 envMap := make(map[string]string)
386
387 scanner := bufio.NewScanner(envInput)
388 var lineNumber int
389
390 for scanner.Scan() {
391 line := strings.TrimSpace(scanner.Text())
392 lineNumber++
393
394 // skip empty lines and lines starting with comment
395 if line == "" || strings.HasPrefix(line, "#") {
396 continue
397 }
398
399 // split line into key and value
400 before, after, isCut := strings.Cut(line, "=")
401 if !isCut {
402 return nil, fmt.Errorf("can't parse line %d; line should be in KEY=VALUE format", lineNumber)
403 }
404 key, val := before, after
405
406 // sometimes keys are prefixed by "export " so file can be sourced in bash; ignore it here
407 key = strings.TrimPrefix(key, "export ")
408
409 // validate key and value
410 if key == "" {
411 return nil, fmt.Errorf("missing or empty key on line %d", lineNumber)
412 }
413 if strings.Contains(key, " ") {
414 return nil, fmt.Errorf("invalid key on line %d: contains whitespace: %s", lineNumber, key)
415 }
416 if strings.HasPrefix(val, " ") || strings.HasPrefix(val, "\t") {
417 return nil, fmt.Errorf("invalid value on line %d: whitespace before value: '%s'", lineNumber, val)
418 }
419
420 // remove any trailing comment after value
421 if commentStart, _, found := strings.Cut(val, "#"); found {
422 val = strings.TrimRight(commentStart, " \t")
423 }
424
425 // quoted value: support newlines
426 if strings.HasPrefix(val, `"`) || strings.HasPrefix(val, "'") {
427 quote := string(val[0])
428 for !strings.HasSuffix(line, quote) || strings.HasSuffix(line, `\`+quote) {
429 val = strings.ReplaceAll(val, `\`+quote, quote)
430 if !scanner.Scan() {
431 break
432 }
433 lineNumber++
434 line = strings.ReplaceAll(scanner.Text(), `\`+quote, quote)
435 val += "\n" + line
436 }
437 val = strings.TrimPrefix(val, quote)
438 val = strings.TrimSuffix(val, quote)
439 }
440
441 envMap[key] = val
442 }
443
444 if err := scanner.Err(); err != nil {
445 return nil, err
446 }
447
448 return envMap, nil
449 }
450
451 func printEnvironment() {
452 _, version := caddy.Version()
453 fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir())
454 fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
455 fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
456 fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
457 fmt.Printf("caddy.Version=%s\n", version)
458 fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
459 fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
460 fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
461 fmt.Printf("runtime.NumCPU=%d\n", runtime.NumCPU())
462 fmt.Printf("runtime.GOMAXPROCS=%d\n", runtime.GOMAXPROCS(0))
463 fmt.Printf("runtime.Version=%s\n", runtime.Version())
464 cwd, err := os.Getwd()
465 if err != nil {
466 cwd = fmt.Sprintf("<error: %v>", err)
467 }
468 fmt.Printf("os.Getwd=%s\n\n", cwd)
469 for _, v := range os.Environ() {
470 fmt.Println(v)
471 }
472 }
473
474 func setResourceLimits(logger *zap.Logger) func() {
475 // Configure the maximum number of CPUs to use to match the Linux container quota (if any)
476 // See https://pkg.go.dev/runtime#GOMAXPROCS
477 undo, err := maxprocs.Set(maxprocs.Logger(logger.Sugar().Infof))
478 if err != nil {
479 logger.Warn("failed to set GOMAXPROCS", zap.Error(err))
480 }
481
482 // Configure the maximum memory to use to match the Linux container quota (if any) or system memory
483 // See https://pkg.go.dev/runtime/debug#SetMemoryLimit
484 _, _ = memlimit.Set(
485 memlimit.WithLogger(
486 slog.New(zapslog.NewHandler(
487 logger.Core(),
488 zapslog.WithName("memlimit"),
489 // the default enables traces at ERROR level, this disables
490 // them by setting it to a level higher than any other level
491 zapslog.AddStacktraceAt(slog.Level(127)),
492 )),
493 ),
494 memlimit.WithProvider(
495 memlimit.ApplyFallback(
496 memlimit.FromCgroup,
497 memlimit.FromSystem,
498 ),
499 ),
500 )
501
502 return undo
503 }
504
505 // StringSlice is a flag.Value that enables repeated use of a string flag.
506 type StringSlice []string
507
508 func (ss StringSlice) String() string { return "[" + strings.Join(ss, ", ") + "]" }
509
510 func (ss *StringSlice) Set(value string) error {
511 *ss = append(*ss, value)
512 return nil
513 }
514
515 // Interface guard
516 var _ flag.Value = (*StringSlice)(nil)