caddy扩展

Caddy 因其模块化架构而易于扩展。大多数类型的 Caddy 扩展(或插件)被称为模块,如果它们扩展或插入到 Caddy 的配置结构中。需要明确的是,Caddy 模块与 Go 模块是不同的(尽管它们也是 Go 模块)。

快速入门

Caddy 模块是任何在导入其包时注册为 Caddy 模块的命名类型。关键在于,模块总是实现了 caddy.Module
 接口,该接口提供了它的名称和构造函数。

在新的 Go 模块中,将以下模板粘贴到 Go 文件中,并自定义你的包名、类型名和 Caddy 模块 ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package mymodule
import "github.com/caddyserver/caddy/v2"
func init() {
    caddy.RegisterModule(Gizmo{})
}
// Gizmo 是一个示例;在这里放置你自己的类型。
type Gizmo struct {
}
// CaddyModule 返回 Caddy 模块信息。
func (Gizmo) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  "foo.gizmo",
        New: func() caddy.Module { return new(Gizmo) },
    }
}
```

然后从你的项目目录运行以下命令,你应该会在列表中看到你的模块:

xcaddy list-modules

foo.gizmo

1
2
3
4
5
6
  
恭喜,你的模块已经注册到 Caddy 中,并且可以在 Caddy 的配置文件中使用,只要它处于相同命名空间的任何地方。

在底层,xcaddy
 实际上是创建了一个新的 Go 模块,该模块需要 Caddy 和你的插件(使用适当的 replace
 来使用你的本地开发版本),然后添加一个导入以确保它被编译:

import _ “github.com/example/mymodule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
## 模块基础  

Caddy 模块:
1. 实现 caddy.Module
 接口以提供 ID 和构造函数。

1. 在适当的命名空间中具有唯一名称。

1. 通常满足某些对宿主模块有意义的接口。

**宿主模块**
(或父模块)是加载/初始化其他模块的模块。它们通常为客模块定义命名空间。

**客模块**
(或子模块)是被加载或初始化的模块。所有模块都是客模块。
## 模块 ID

每个 Caddy 模块都有一个唯一的 ID,由命名空间和名称组成:
- 完整的 ID 看起来像 foo.bar.module_name


- 命名空间是 foo.bar


- 名称是 module_name
,它必须在其命名空间中是唯一的。

模块 ID 必须使用 snake_case
 约定。
### 命名空间

命名空间类似于类,即命名空间定义了该命名空间中所有模块的共同功能。例如,我们可以期望 http.handlers
 命名空间中的所有模块都是 HTTP 处理程序。因此,宿主模块可能会将命名空间中的客模块从 interface{}
 类型断言为更具体、更有用的类型,例如 caddyhttp.MiddlewareHandler


客模块必须正确命名空间,以便宿主模块能够识别它,因为宿主模块会要求 Caddy 提供某个命名空间中的模块,以提供宿主模块所需的特定功能。例如,如果你编写了一个名为 gizmo
 的 HTTP 处理程序模块,你的模块名称将是 http.handlers.gizmo
,因为 http
 应用程序会在 http.handlers
 命名空间中查找处理程序。

换句话说,Caddy 模块被期望根据其模块命名空间实现某些接口。按照这种约定,模块开发者可以说一些直观的话,例如:“http.handlers
 命名空间中的所有模块都是 HTTP 处理程序。”更技术地说,这通常意味着:“http.handlers
 命名空间中的所有模块都实现了 caddyhttp.MiddlewareHandler
 接口。”因为方法集是已知的,所以可以断言并使用更具体的类型。

**查看将所有标准 Caddy 命名空间映射到其 Go 类型的表格。**

caddy
 和 admin
 命名空间是保留的,不能用作应用名称。

要编写插入第三方宿主模块的模块,请查阅这些模块的命名空间文档。
### 名称

命名空间内的名称很重要,对用户也很明显,但只要它是唯一的、简洁的,并且符合它的功能,它就不那么重要。
## 应用模块

应用是具有空命名空间的模块,并且按照惯例成为它们自己的顶级命名空间。应用模块实现了 caddy.App
 接口。

这些模块出现在 Caddy 配置的顶级 "apps"
 属性中:

{
    “apps”: {}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  
示例应用是 http
 和 tls
。它们的命名空间是空的。

为这些应用编写的客模块应该使用从应用名称派生的命名空间。例如,HTTP 处理程序使用 http.handlers
 命名空间,TLS 证书加载器使用 tls.certificates
 命名空间。
## 模块实现

模块可以是几乎任何类型,但结构体是最常见的,因为它们可以保存用户配置。
### 配置

大多数模块需要一些配置。只要你的类型与 JSON 兼容,Caddy 就会自动处理这些配置。因此,如果模块是结构体类型,它将需要在其字段上使用结构体标签,这些标签应该使用 Caddy 约定的 snake_case

type Gizmo struct {
    MyField string json:"my_field,omitempty"
    Number  int    json:"number,omitempty"
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  
在结构体标签中使用 omitempty
 选项将在 JSON 输出中省略该字段,如果它是其类型的零值。这有助于在将 JSON 配置从 Caddyfile 转换为 JSON 时保持 JSON 配置的简洁。

当模块被初始化时,它已经填充了其配置。也可以在模块初始化后执行额外的配置和验证步骤。
### 模块生命周期

模块的生命周期从它被宿主模块加载时开始。以下是发生的情况:
1. 调用 New()
 方法以获取模块值的实例。

1. 将模块的配置反序列化到该实例中。

1. 如果模块是 caddy.Provisioner
,则调用 Provision()
 方法。

1. 如果模块是 caddy.Validator
,则调用 Validate()
 方法。

1. 此时,宿主模块将得到已加载的客模块作为 interface{}
 类型的值,因此宿主模块通常会将客模块断言为更有用的类型。请查阅宿主模块的文档,了解其命名空间中的客模块需要满足什么要求,例如需要实现哪些方法。

1. 当模块不再需要时,如果它是 caddy.CleanerUpper
,则调用 Cleanup()
 方法。

请注意,在给定时间,你的模块的多个加载实例可能会重叠!在配置更改期间,新模块会在旧模块停止之前启动。在使用全局状态时要格外小心。使用 caddy.UsagePool
 类型来帮助管理模块加载之间的全局状态。如果你的模块监听一个套接字,请使用 caddy.Listen*()
 来获取支持重叠使用的套接字。
### 配置

模块的配置将自动反序列化到其值中(当加载 JSON 配置时)。这意味着,例如,结构体字段将为你填充。

然而,如果模块需要额外的配置步骤,你可以实现可选的 caddy.Provisioner
 接口:

// Provision 设置模块。
func (g *Gizmo) Provision(ctx caddy.Context) error {
    // TODO: 设置模块
    return nil
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  
这是设置用户未提供的字段的默认值的地方(不是其类型的零值的字段)。如果字段是必需的,如果它未设置,你可以返回一个错误。对于具有零值含义的数字字段(例如,某些超时持续时间),你可能希望支持 -1
 表示“关闭”,而不是 0
,因此如果你的用户未配置它,你可以设置一个默认值。

这也是宿主模块通常加载它们的客/子模块的地方。

模块可以通过调用 ctx.App()
 访问其他应用,但模块不能有循环依赖。换句话说,由 http
 应用程序加载的模块不能依赖于 tls
 应用程序,如果由 tls
 应用程序加载的模块依赖于 http
 应用程序。(这与 Go 中禁止导入循环的规则非常相似。)

此外,在 Provision
 中应避免执行昂贵的操作,因为即使配置仅被验证,也会执行配置。在配置阶段,不要期望模块实际上会被使用。
#### 日志

了解 Caddy 中的日志记录方式。如果模块需要日志记录,请不要使用 Go 标准库中的 log.Print*()
。换句话说,**不要使用 Go 的全局日志记录器**
。Caddy 使用高性能、高度灵活的结构化
日志记录,使用 zap。

要在模块的 Provision 方法中发出日志,请获取一个日志记录器:

func (g *Gizmo) Provision(ctx caddy.Context) error {
    g.logger = ctx.Logger() // g.logger 是一个 *zap.Logger
}

1
2
3
4
5
6
7
  
然后你可以使用 g.logger
 发出结构化、分级的日志。有关详细信息,请参阅 zap 的 godoc。
### 验证

希望验证其配置的模块可以通过实现可选的 caddy.Validator
 接口来实现:

// Validate 验证模块是否具有可用的配置。
func (g Gizmo) Validate() error {
    // TODO: 验证模块的设置
    return nil
}

1
2
3
4
5
6
7
8
  
Validate 应该是一个只读函数。它在 Provision()
 方法之后运行。
### 接口保护

Caddy 模块的行为是隐式的,因为 Go 接口是隐式满足的。只需在模块的类型中添加正确的方法,就可以使模块正确或错误地工作。因此,打错字或方法签名错误可能导致意外的(缺乏)行为。

幸运的是,有一个简单、无开销、编译时检查可以添加到代码中,以确保你添加了正确的方法。这些称为接口保护:

var _ InterfaceName = (*YourType)(nil)

1
2
3
4
5
6
  
将 InterfaceName
 替换为你打算满足的接口,将 YourType
 替换为你的模块类型的名称。

例如,像静态文件服务器这样的 HTTP 处理程序可能满足多个接口:

// 接口保护
var (
    _ caddy.Provisioner           = (*FileServer)(nil)
    _ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
)

1
2
3
4
5
6
7
8
  
如果没有接口保护,可能会出现令人困惑的错误。例如,如果模块在使用之前需要进行配置,但你的 Provision()
 方法有错误(例如拼写错误或签名错误),则不会进行配置,导致令人困惑的问题。接口保护很容易添加,并且可以防止这种情况。它们通常放在文件的底部。
## 宿主模块

当模块加载自己的客模块时,它就成为了一个宿主模块。如果模块的功能可以以不同的方式实现,这是很有用的。

宿主模块几乎总是一个结构体。通常,支持客模块需要两个结构体字段:一个用于保存其原始 JSON,另一个用于保存其解码后的值:

type Gizmo struct {
    GadgetRaw json.RawMessage json:"gadget,omitempty" caddy:"namespace=foo.gizmo.gadgets inline_key=gadgeter"
    Gadget Gadgeter json:"-"
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  
第一个字段(在这个例子中是 GadgetRaw
)是原始的、未配置的 JSON 形式的客模块所在的地方。

第二个字段(Gadget
)是最终配置后的值将被存储的地方。由于第二个字段不是面向用户的,我们使用结构体标签将其从 JSON 中排除。(如果你不需要其他包使用它,你也可以将其设为未导出字段,那么就不需要结构体标签了。)
### Caddy 结构体标签

原始模块字段上的 caddy
 结构体标签帮助 Caddy 知道要加载的模块的命名空间和名称(组成完整的 ID)。它还用于生成文档。

结构体标签的格式非常简单:key1=val1 key2=val2 ...

对于模块字段,结构体标签看起来像:

caddy:"namespace=foo.bar inline_key=baz"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  
namespace=
 部分是必需的。它定义了要查找模块的命名空间。

inline_key=
 部分仅在模块的名称将与模块本身**内联**
时使用;这意味着值是一个对象,其中一个键是**内联键**
,其值是模块的名称。如果省略,则字段类型必须是 caddy.ModuleMap
 或 []caddy.ModuleMap
,其中映射的键是模块名称。
### 加载客模块

在配置阶段调用 ctx.LoadModule()
 来加载客模块:

// Provision 设置 g 并加载其小工具。
func (g *Gizmo) Provision(ctx caddy.Context) error {
    if g.GadgetRaw != nil {
        val, err := ctx.LoadModule(g, “GadgetRaw”)
        if err != nil {
            return fmt.Errorf(“loading gadget module: %v”, err)
        }
        g.Gadget = val.(Gadgeter)
    }
    return nil
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  
注意,LoadModule()
 调用接受一个指向结构体的指针和字段名作为字符串。这看起来很奇怪,不是吗?为什么不直接传递结构体字段呢?这是因为根据配置的布局,有几种不同的方式可以加载模块。这种方法签名允许 Caddy 使用反射来确定加载模块的最佳方式,并且最重要的是,读取其结构体标签。

如果客模块必须明确由用户设置,你应该在尝试加载它之前返回一个错误,如果 Raw 字段为 nil 或为空。

注意如何断言加载的模块:g.Gadget = val.(Gadgeter)
 — 这是因为返回的 val
 是一个 interface{}
 类型,这并不太有用。然而,我们期望在声明的命名空间中(在我们的例子中是 foo.gizmo.gadgets
,来自结构体标签)的所有模块都实现了 Gadgeter
 接口,因此这种类型断言是安全的,然后我们就可以使用它了!

如果你的宿主模块定义了一个新的命名空间,请确保像我们在这里所做的那样为开发者记录该命名空间及其 Go 类型。
## 模块文档

注册模块以使新的 Caddy 模块出现在模块文档中,并可在 
http://caddyserver.com/download
 中使用。注册可在 
http://caddyserver.com/account
 进行。如果你还没有账户,请创建一个新账户,然后点击“注册包”。
## 完整示例

假设我们想编写一个 HTTP 处理程序模块。这将是一个演示用途的中间件,它会在每次 HTTP 请求时将访问者的 IP 地址打印到一个流中。

我们还希望它可以通过 Caddyfile 配置,因为大多数人在非自动化情况下更喜欢使用 Caddyfile。我们通过注册一个 Caddyfile 处理程序指令来实现这一点,这是一种可以向 HTTP 路由添加处理程序的指令。我们还实现了 caddyfile.Unmarshaler
 接口。通过添加这几行代码,这个模块可以通过 Caddyfile 配置!例如:visitor_ip stdout


以下是该模块的代码,带有解释性注释:

package visitorip
import (
    “fmt”
    “io”
    “net/http”
    “os”
    “github.com/caddyserver/caddy/v2
    “github.com/caddyserver/caddy/v2/caddyconfig/caddyfile
    “github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile
    “github.com/caddyserver/caddy/v2/modules/caddyhttp
)
func init() {
    caddy.RegisterModule(Middleware{})
    httpcaddyfile.RegisterHandlerDirective(“visitor_ip”, parseCaddyfile)
}
// Middleware 实现了一个 HTTP 处理程序,它将访问者的 IP 地址写入文件或流。
type Middleware struct {
    // 要写入的文件或流。可以是 “stdout”
    // 或 “stderr”。
    Output string json:"output,omitempty"
    w io.Writer
}
// CaddyModule 返回 Caddy 模块信息。
func (Middleware) CaddyModule() caddy.ModuleInfo {
    return caddy.ModuleInfo{
        ID:  “http.handlers.visitor_ip”,
        New: func() caddy.Module { return new(Middleware) },
    }
}
// Provision 实现了 caddy.Provisioner。
func (m *Middleware) Provision(ctx caddy.Context) error {
    switch m.Output {
    case “stdout”:
        m.w = os.Stdout
    case “stderr”:
        m.w = os.Stderr
    default:
        return fmt.Errorf(“需要一个输出流”)
    }
    return nil
}
// Validate 实现了 caddy.Validator。
func (m Middleware) Validate() error {
    if m.w == nil {
        return fmt.Errorf(“没有写入器”)
    }
    return nil
}
// ServeHTTP 实现了 caddyhttp.MiddlewareHandler。
func (m Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
    m.w.Write([]byte(r.RemoteAddr))
    return next.ServeHTTP(w, r)
}
// UnmarshalCaddyfile 实现了 caddyfile.Unmarshaler。
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
    d.Next() // 消耗指令名称
    // 需要一个参数
    if !d.NextArg() {
        return d.ArgErr()
    }
    // 保存参数
    m.Output = d.Val()
    return nil
}
// parseCaddyfile 从 h 中反序列化标记到新的 Middleware。
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
    var m Middleware
    err := m.UnmarshalCaddyfile(h.Dispenser)
    return m, err
}
// 接口保护
var (
    _ caddy.Provisioner           = (*Middleware)(nil)
    _ caddy.Validator             = (*Middleware
)(nil)
    _ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
    _ caddyfile.Unmarshaler       = (*Middleware)(nil)
)

  
  

![江达小记](/images/wechatmpscan.png)