前言

本次系列会讲解 caddy 整个生命周期涉及到的源码。
image.png

平时我们使用 caddy 都是使用 它的 二进制 分发文件,现在来分析 caddy 的 Run 函数。

Run 在上图中是 最右的箭头 StartServer

我们从最外层逻辑看它都做了些什么。

Caddy Run

我们来看看 Caddy Run 中引入了哪些包和操作,对 Caddy 的总体行为做一个概览
caddy/caddymain/run.go
首先看 init 函数

  1. func init() {
  2. caddy.TrapSignals()
  3. flag.BoolVar(&certmagic.Default.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
  4. flag.StringVar(&certmagic.Default.CA, "ca", certmagic.Default.CA, "URL to certificate authority's ACME server directory")
  5. flag.StringVar(&certmagic.Default.DefaultServerName, "default-sni", certmagic.Default.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
  6. flag.BoolVar(&certmagic.Default.DisableHTTPChallenge, "disable-http-challenge", certmagic.Default.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
  7. flag.BoolVar(&certmagic.Default.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.Default.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
  8. flag.StringVar(&certmagic.Default.Email, "email", "", "Default ACME CA account email address")
  9. flag.DurationVar(&certmagic.HTTPTimeout, "catimeout", certmagic.HTTPTimeout, "Default ACME CA HTTP timeout")
  10. flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
  11. flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
  12. flag.StringVar(&conf, "conf", "", "Caddyfile to load (default \""+caddy.DefaultConfigFile+"\")")
  13. flag.StringVar(&cpu, "cpu", "100%", "CPU cap")
  14. flag.BoolVar(&printEnv, "env", false, "Enable to print environment variables")
  15. flag.StringVar(&envFile, "envfile", "", "Path to file with environment variables to load in KEY=VALUE format")
  16. flag.BoolVar(&fromJSON, "json-to-caddyfile", false, "From JSON stdin to Caddyfile stdout")
  17. flag.BoolVar(&toJSON, "caddyfile-to-json", false, "From Caddyfile stdin to JSON stdout")
  18. flag.BoolVar(&plugins, "plugins", false, "List installed plugins")
  19. flag.StringVar(&logfile, "log", "", "Process log file")
  20. flag.IntVar(&logRollMB, "log-roll-mb", 100, "Roll process log when it reaches this many megabytes (0 to disable rolling)")
  21. flag.BoolVar(&logRollCompress, "log-roll-compress", true, "Gzip-compress rolled process log files")
  22. flag.StringVar(&caddy.PidFile, "pidfile", "", "Path to write pid file")
  23. flag.BoolVar(&caddy.Quiet, "quiet", false, "Quiet mode (no initialization output)")
  24. flag.StringVar(&serverType, "type", "http", "Type of server to run")
  25. flag.BoolVar(&version, "version", false, "Show version")
  26. flag.BoolVar(&validate, "validate", false, "Parse the Caddyfile but do not start the server")
  27. caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
  28. caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))
  29. }

然后我们来看 Run() 函数 Run() 函数全文可在最下看到

conf 的灵活设置

conf 用来设置 Caddyfile 的文件路径,是可以从 Stdin 即终端输入配置的。
会在以下两个在 init() 中的函数调用更改

  1. caddy.RegisterCaddyfileLoader("flag", caddy.LoaderFunc(confLoader))
  2. caddy.SetDefaultCaddyfileLoader("default", caddy.LoaderFunc(defaultLoader))

而注意到这里使用的 Loader ,可以用来自定义 caddyfile 的装载方式。

  1. // confLoader loads the Caddyfile using the -conf flag.
  2. func confLoader(serverType string) (caddy.Input, error) {
  3. if conf == "" {
  4. return nil, nil
  5. }
  6. if conf == "stdin" {
  7. return caddy.CaddyfileFromPipe(os.Stdin, serverType)
  8. }
  9. var contents []byte
  10. if strings.Contains(conf, "*") {
  11. // Let caddyfile.doImport logic handle the globbed path
  12. contents = []byte("import " + conf)
  13. } else {
  14. var err error
  15. contents, err = ioutil.ReadFile(conf)
  16. if err != nil {
  17. return nil, err
  18. }
  19. }
  20. return caddy.CaddyfileInput{
  21. Contents: contents,
  22. Filepath: conf,
  23. ServerTypeName: serverType,
  24. }, nil
  25. }

注意到这里返回的 caddy.Input 类型,它代表 caddyfile 在程序中的 structure

Log

log 设置 日志 输出在什么地方,log-roll-mb log-roll-compress 是设置 log 文件大小限制,达到限制的时候会放弃旧的日志。还有 文件的压缩选项

  1. // Set up process log before anything bad happens
  2. switch logfile {
  3. case "stdout":
  4. log.SetOutput(os.Stdout)
  5. case "stderr":
  6. log.SetOutput(os.Stderr)
  7. case "":
  8. log.SetOutput(ioutil.Discard)
  9. default:
  10. if logRollMB > 0 {
  11. log.SetOutput(&lumberjack.Logger{
  12. Filename: logfile,
  13. MaxSize: logRollMB,
  14. MaxAge: 14,
  15. MaxBackups: 10,
  16. Compress: logRollCompress,
  17. })
  18. } else {
  19. err := os.MkdirAll(filepath.Dir(logfile), 0755)
  20. if err != nil {
  21. mustLogFatalf("%v", err)
  22. }
  23. f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  24. if err != nil {
  25. mustLogFatalf("%v", err)
  26. }
  27. // don't close file; log should be writeable for duration of process
  28. log.SetOutput(f)
  29. }
  30. }

证书处理

我们看前 8 个 flag 选项,都是设定 TLS 配置的。使用的是 CertMagic ,同时作者也把它放出来可以被其他的 Go 语言程序集成,如果想要为自己的 Go 程序添加 TLS 加密传输,就使用它吧。他还支持自动续期,本身是使用的 ACME 客户端集成。
本身大部分不需要设置,会使用默认设置帮助你启用 HTTPS 。
介绍一下 revoke 选项,这里会调用 CertMagic 的 Certificate revocation (please, only if private key is compromised)来撤销证书

  1. // Check for one-time actions
  2. if revoke != "" {
  3. err := caddytls.Revoke(revoke)
  4. if err != nil {
  5. mustLogFatalf("%v", err)
  6. }
  7. fmt.Printf("Revoked certificate for %s\n", revoke)
  8. os.Exit(0)
  9. }

telemetry

然后是 disabled-metrics 选项,用来关闭一些不需要的 遥测指标
注意到这里的 initTelemetry() 函数,其中会使用 这里输入的选项进行遥测的关闭。

  1. // initialize telemetry client
  2. if EnableTelemetry {
  3. err := initTelemetry()
  4. if err != nil {
  5. mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
  6. }
  7. } else if disabledMetrics != "" {
  8. mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
  9. }

然后会在 Run() 的最后部分进行相应遥测模块的 设置

  1. // Begin telemetry (these are no-ops if telemetry disabled)
  2. telemetry.Set("caddy_version", module.Version)
  3. telemetry.Set("num_listeners", len(instance.Servers()))
  4. telemetry.Set("server_type", serverType)
  5. telemetry.Set("os", runtime.GOOS)
  6. telemetry.Set("arch", runtime.GOARCH)
  7. telemetry.Set("cpu", struct {
  8. BrandName string `json:"brand_name,omitempty"`
  9. NumLogical int `json:"num_logical,omitempty"`
  10. AESNI bool `json:"aes_ni,omitempty"`
  11. }{
  12. BrandName: cpuid.CPU.BrandName,
  13. NumLogical: runtime.NumCPU(),
  14. AESNI: cpuid.CPU.AesNi(),
  15. })
  16. if containerized := detectContainer(); containerized {
  17. telemetry.Set("container", containerized)
  18. }
  19. telemetry.StartEmitting()

环境设置

然后是环境变量的设置 envenvfile,分别是打印出环境信息和可以设置环境变量文件的位置

  1. // load all additional envs as soon as possible
  2. if err := LoadEnvFromFile(envFile); err != nil {
  3. mustLogFatalf("%v", err)
  4. }
  5. if printEnv {
  6. for _, v := range os.Environ() {
  7. fmt.Println(v)
  8. }
  9. }

cpu

cpu 设定 cpu 使用限制

  1. // Set CPU cap
  2. err := setCPU(cpu)
  3. if err != nil {
  4. mustLogFatalf("%v", err)
  5. }

分发事件

这是用来启用插件的。

  1. // Executes Startup events
  2. caddy.EmitEvent(caddy.StartupEvent, nil)

服务器类型

type 设定 caddy 启动的服务器类型,这是因为 caddy 能启动的服务器很多,只要满足了 Server 的接口就可以使用 caddy。
这里调用的 LoadCaddyfile 可以认为是 Caddy 配置最重要的部分。

  1. // Get Caddyfile input
  2. caddyfileinput, err := caddy.LoadCaddyfile(serverType)
  3. if err != nil {
  4. mustLogFatalf("%v", err)
  5. }

pidfile 设置写入 pid 文件的路径, quiet 设置 caddy 不输出初始化信息的启动 。

version 和 plugin 信息

version 用来显示 caddy 版本,plugins 用来列出已经安装的 插件

  1. if version {
  2. if module.Sum != "" {
  3. // a build with a known version will also have a checksum
  4. fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
  5. } else {
  6. fmt.Println(module.Version)
  7. }
  8. os.Exit(0)
  9. }
  10. if plugins {
  11. fmt.Println(caddy.DescribePlugins())
  12. os.Exit(0)
  13. }

validate 验证 caddyfile 文件

validate 是验证 caddyfile 的可行性,他会检测 caddyfile 但是不会启动一个新的 Server。注意到在这里调用了 os.Exit(0) 退出。

  1. if validate {
  2. err := caddy.ValidateAndExecuteDirectives(caddyfileinput, nil, true)
  3. if err != nil {
  4. mustLogFatalf("%v", err)
  5. }
  6. msg := "Caddyfile is valid"
  7. fmt.Println(msg)
  8. log.Printf("[INFO] %s", msg)
  9. os.Exit(0)
  10. }

caddyfile 与 JSON

json-to-caddyfilecaddyfile-to-json 从 JSON 文件中读取 caddyfile 的配置文件,或者将 caddyfile 文件输出为 JSON 格式。

  1. // Check if we just need to do a Caddyfile Convert and exit
  2. checkJSONCaddyfile()

根据 flag 选项进行 Caddyfile 和 JSON 直接的转换。

  1. func checkJSONCaddyfile() {
  2. if fromJSON {
  3. jsonBytes, err := ioutil.ReadAll(os.Stdin)
  4. if err != nil {
  5. fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
  6. os.Exit(1)
  7. }
  8. caddyfileBytes, err := caddyfile.FromJSON(jsonBytes)
  9. if err != nil {
  10. fmt.Fprintf(os.Stderr, "Converting from JSON failed: %v", err)
  11. os.Exit(2)
  12. }
  13. fmt.Println(string(caddyfileBytes))
  14. os.Exit(0)
  15. }
  16. if toJSON {
  17. caddyfileBytes, err := ioutil.ReadAll(os.Stdin)
  18. if err != nil {
  19. fmt.Fprintf(os.Stderr, "Read stdin failed: %v", err)
  20. os.Exit(1)
  21. }
  22. jsonBytes, err := caddyfile.ToJSON(caddyfileBytes)
  23. if err != nil {
  24. fmt.Fprintf(os.Stderr, "Converting to JSON failed: %v", err)
  25. os.Exit(2)
  26. }
  27. fmt.Println(string(jsonBytes))
  28. os.Exit(0)
  29. }
  30. }

Start

启动服务器

  1. // Start your engines
  2. instance, err := caddy.Start(caddyfileinput)
  3. if err != nil {
  4. mustLogFatalf("%v", err)
  5. }

总结

看完 caddy 的 run 函数,了解了程序的启动配置了哪些东西,相应的会有一些问题。带着这些问题继续看源码,能够深入了解 caddy 内的逻辑。

  • caddy.Input 的内在读取 caddyfile 的操作
  • Loader 可以看出也是可以自定义的,他实际涉及到 Caddy 较为重要的 接收配置,安装配置的操作部分。它会读取 token 给 plugin 配置时 给 controller 消费,这里多的名词是来自于 caddy 的配置部分
  • caddy.EmitEvent 是怎样启动各 caddy 插件的
  • caddy.ServerType 除了 HTTP 还有什么(插件会对应相应的服务器的)
  • instance 可以看出是 caddy 服务器的实例,它具体的设置逻辑是什么来获得 插件装配的能力的。他是怎样作为服务器服务的
  • 遥测模块的接入(caddy 2已经不用了)
  • 其他的就是一些方便的选项了。

附件

Run() 函数源码

  1. // Run is Caddy's main() function.
  2. func Run() {
  3. flag.Parse()
  4. module := getBuildModule()
  5. cleanModVersion := strings.TrimPrefix(module.Version, "v")
  6. caddy.AppName = appName
  7. caddy.AppVersion = module.Version
  8. certmagic.UserAgent = appName + "/" + cleanModVersion
  9. // Set up process log before anything bad happens
  10. switch logfile {
  11. case "stdout":
  12. log.SetOutput(os.Stdout)
  13. case "stderr":
  14. log.SetOutput(os.Stderr)
  15. case "":
  16. log.SetOutput(ioutil.Discard)
  17. default:
  18. if logRollMB > 0 {
  19. log.SetOutput(&lumberjack.Logger{
  20. Filename: logfile,
  21. MaxSize: logRollMB,
  22. MaxAge: 14,
  23. MaxBackups: 10,
  24. Compress: logRollCompress,
  25. })
  26. } else {
  27. err := os.MkdirAll(filepath.Dir(logfile), 0755)
  28. if err != nil {
  29. mustLogFatalf("%v", err)
  30. }
  31. f, err := os.OpenFile(logfile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
  32. if err != nil {
  33. mustLogFatalf("%v", err)
  34. }
  35. // don't close file; log should be writeable for duration of process
  36. log.SetOutput(f)
  37. }
  38. }
  39. // load all additional envs as soon as possible
  40. if err := LoadEnvFromFile(envFile); err != nil {
  41. mustLogFatalf("%v", err)
  42. }
  43. if printEnv {
  44. for _, v := range os.Environ() {
  45. fmt.Println(v)
  46. }
  47. }
  48. // initialize telemetry client
  49. if EnableTelemetry {
  50. err := initTelemetry()
  51. if err != nil {
  52. mustLogFatalf("[ERROR] Initializing telemetry: %v", err)
  53. }
  54. } else if disabledMetrics != "" {
  55. mustLogFatalf("[ERROR] Cannot disable specific metrics because telemetry is disabled")
  56. }
  57. // Check for one-time actions
  58. if revoke != "" {
  59. err := caddytls.Revoke(revoke)
  60. if err != nil {
  61. mustLogFatalf("%v", err)
  62. }
  63. fmt.Printf("Revoked certificate for %s\n", revoke)
  64. os.Exit(0)
  65. }
  66. if version {
  67. if module.Sum != "" {
  68. // a build with a known version will also have a checksum
  69. fmt.Printf("Caddy %s (%s)\n", module.Version, module.Sum)
  70. } else {
  71. fmt.Println(module.Version)
  72. }
  73. os.Exit(0)
  74. }
  75. if plugins {
  76. fmt.Println(caddy.DescribePlugins())
  77. os.Exit(0)
  78. }
  79. // Check if we just need to do a Caddyfile Convert and exit
  80. checkJSONCaddyfile()
  81. // Set CPU cap
  82. err := setCPU(cpu)
  83. if err != nil {
  84. mustLogFatalf("%v", err)
  85. }
  86. // Executes Startup events
  87. caddy.EmitEvent(caddy.StartupEvent, nil)
  88. // Get Caddyfile input
  89. caddyfileinput, err := caddy.LoadCaddyfile(serverType)
  90. if err != nil {
  91. mustLogFatalf("%v", err)
  92. }
  93. if validate {
  94. err := caddy.ValidateAndExecuteDirectives(caddyfileinput, nil, true)
  95. if err != nil {
  96. mustLogFatalf("%v", err)
  97. }
  98. msg := "Caddyfile is valid"
  99. fmt.Println(msg)
  100. log.Printf("[INFO] %s", msg)
  101. os.Exit(0)
  102. }
  103. // Log Caddy version before start
  104. log.Printf("[INFO] Caddy version: %s", module.Version)
  105. // Start your engines
  106. instance, err := caddy.Start(caddyfileinput)
  107. if err != nil {
  108. mustLogFatalf("%v", err)
  109. }
  110. // Begin telemetry (these are no-ops if telemetry disabled)
  111. telemetry.Set("caddy_version", module.Version)
  112. telemetry.Set("num_listeners", len(instance.Servers()))
  113. telemetry.Set("server_type", serverType)
  114. telemetry.Set("os", runtime.GOOS)
  115. telemetry.Set("arch", runtime.GOARCH)
  116. telemetry.Set("cpu", struct {
  117. BrandName string `json:"brand_name,omitempty"`
  118. NumLogical int `json:"num_logical,omitempty"`
  119. AESNI bool `json:"aes_ni,omitempty"`
  120. }{
  121. BrandName: cpuid.CPU.BrandName,
  122. NumLogical: runtime.NumCPU(),
  123. AESNI: cpuid.CPU.AesNi(),
  124. })
  125. if containerized := detectContainer(); containerized {
  126. telemetry.Set("container", containerized)
  127. }
  128. telemetry.StartEmitting()
  129. // Twiddle your thumbs
  130. instance.Wait()
  131. }

image.png