扩展 Caddy
由于 Caddy 的模块化架构,它很容易扩展。大多数类型的 Caddy 扩展(或插件)如果扩展或插入到 Caddy 的配置结构中,就被称为_模块_。需要明确的是,Caddy 模块与 Go 模块不同(但它们也是 Go 模块)。
前提条件:
快速入门
Caddy 模块是任何在导入其包时将自己注册为 Caddy 模块的命名类型。关键的是,模块总是实现 caddy.Module
接口,该接口提供其名称和构造函数。
在新的 Go 模块中,将以下模板粘贴到 Go 文件中,并自定义你的包名、类型名和 Caddy 模块 ID:
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
...
恭喜,你的模块已注册到 Caddy,可以在 Caddy 的配置文档 中使用,在任何使用相同命名空间中模块的地方。
在底层,xcaddy
只是创建一个新的 Go 模块,该模块同时需要 Caddy 和你的插件(使用适当的 replace
来使用你的本地开发版本),然后添加一个导入以确保它被编译进去:
import _ "github.com/example/mymodule"
模块基础
Caddy 模块:
- 实现
caddy.Module
接口以提供 ID 和构造函数 - 在适当的命名空间中具有唯一的名称
- 通常满足对该命名空间的主机模块有意义的一个或多个接口
主机模块(或_父模块_)是加载/初始化其他模块的模块。它们通常为访客模块定义命名空间。
访客模块(或_子模块_)是被加载或初始化的模块。所有模块都是访客模块。
模块 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": {}
}
应用 的示例是 http
和 tls
。它们的命名空间是空的。
为这些应用编写的访客模块应该在从应用名称派生的命名空间中。例如,HTTP 处理器使用 http.handlers
命名空间,TLS 证书加载器使用 tls.certificates
命名空间。
模块实现
模块几乎可以是任何类型,但结构体最常见,因为它们可以保存用户配置。
配置
大多数模块需要一些配置。只要你的类型与 JSON 兼容,Caddy 就会自动处理这个问题。因此,如果模块是结构体类型,它需要在其字段上有结构体标签,根据 Caddy 约定应该使用 snake_casing
:
type Gizmo struct {
MyField string `json:"my_field,omitempty"`
Number int `json:"number,omitempty"`
}
在结构体标签中使用 omitempty
选项将在字段为其类型的零值时从 JSON 输出中省略该字段。这在编组时(例如,从 Caddyfile 适配到 JSON)保持 JSON 配置干净简洁很有用。
当模块初始化时,它已经填充了其配置。在模块初始化后,还可以执行额外的配置和验证步骤。
模块生命周期
模块的生命从被主机模块加载开始。发生以下情况:
- 调用
New()
获取模块值的实例。 - 模块的配置被解组到该实例中。
- 如果模块是
caddy.Provisioner
,则调用Provision()
方法。 - 如果模块是
caddy.Validator
,则调用Validate()
方法。 - 此时,主机模块获得加载的访客模块作为
interface{}
值,因此主机模块通常会将访客模块类型断言为更有用的类型。查看主机模块的文档以了解其命名空间中访客模块的要求,例如需要实现哪些方法。 - 当不再需要模块时,如果它是
caddy.CleanerUpper
,则调用Cleanup()
方法。
注意,你的模块的多个加载实例可能在给定时间重叠!在配置更改期间,新模块在旧模块停止之前启动。请务必谨慎使用全局状态。使用 caddy.UsagePool
类型来帮助管理模块加载之间的全局状态。如果你的模块监听套接字,使用 caddy.Listen*()
获取支持重叠使用的套接字。
配置
模块的配置将自动解组到其值中(加载 JSON 配置时)。这意味着,例如,结构体字段将为你填充。
但是,如果你的模块需要额外的配置步骤,你可以实现(可选的)caddy.Provisioner
接口:
// Provision 设置模块。
func (g *Gizmo) Provision(ctx caddy.Context) error {
// TODO: 设置模块
return nil
}
这是你应该为用户未提供的字段(不是其零值的字段)设置默认值的地方。如果字段是必需的,如果未设置,你可以返回错误。对于零值有意义的数字字段(例如,某些超时持续时间),你可能想要支持 -1
表示"关闭"而不是 0
,所以如果用户没有配置它,你可以设置默认值。
这通常也是主机模块加载其访客/子模块的地方。
模块可以通过调用 ctx.App()
访问其他应用,但模块不能有循环依赖。换句话说,如果 tls
应用加载的模块依赖于 http
应用,则 http
应用加载的模块不能依赖于 tls
应用。(非常类似于 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
}
然后你可以使用 g.logger
发出结构化的、分级的日志。有关详细信息,请参阅 zap 的 godoc。
验证
想要验证其配置的模块可以通过满足(可选的)caddy.Validator
接口来实现:
// Validate 验证模块是否有可用的配置。
func (g Gizmo) Validate() error {
// TODO: 验证模块的设置
return nil
}
Validate 应该是一个只读函数。它在 Provision()
方法之后运行。
接口守卫
Caddy 模块行为是隐式的,因为 Go 接口是隐式满足的。只需将正确的方法添加到模块的类型中,就可以使模块正确或错误。因此,拼写错误或方法签名错误可能导致意外的(缺乏)行为。
幸运的是,你可以添加一个简单的、无开销的编译时检查到你的代码中,以确保你添加了正确的方法。这些被称为接口守卫:
var _ InterfaceName = (*YourType)(nil)
将 InterfaceName
替换为你打算满足的接口,将 YourType
替换为模块类型的名称。
例如,像静态文件服务器这样的 HTTP 处理器可能满足多个接口:
// 接口守卫
var (
_ caddy.Provisioner = (*FileServer)(nil)
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
)
如果 *FileServer
不满足这些接口,这将阻止程序编译。
没有接口守卫,可能会引入令人困惑的错误。例如,如果你的模块必须在使用前配置自己,但你的 Provision()
方法有错误(例如,拼写错误或签名错误),配置将永远不会发生,导致令人困惑的问题。接口守卫非常简单,可以防止这种情况。它们通常放在文件的底部。
主机模块
当模块加载自己的访客模块时,它就成为了主机模块。如果模块的某些功能可以用不同方式实现,这很有用。
主机模块几乎总是一个结构体。通常,支持访客模块需要两个结构体字段:一个用于保存其原始 JSON,另一个用于保存其解码值:
type Gizmo struct {
GadgetRaw json.RawMessage `json:"gadget,omitempty" caddy:"namespace=foo.gizmo.gadgets inline_key=gadgeter"`
Gadget Gadgeter `json:"-"`
}
The first field (GadgetRaw
in this example) is where the raw, unprovisioned JSON form of the guest module can be found.
The second field (Gadget
) is where the final, provisioned value will eventually be stored. Since the second field is not user-facing, we exclude it from JSON with a struct tag. (You could also unexport it if it is not needed by other packages, and then no struct tag is needed.)
Caddy struct tags
The caddy
struct tag on the raw module field helps Caddy to know the namespace and name (comprising the complete ID) of the module to load. It is also used for generating documentation.
The struct tag has a very simple format: key1=val1 key2=val2 ...
For module fields, the struct tag will look like:
`caddy:"namespace=foo.bar inline_key=baz"`
The namespace=
part is required. It defines the namespace in which to look for the module.
The inline_key=
part is only used if the module's name will be found inline with the module itself; this implies that the value is an object where one of the keys is the inline key, and its value is the name of the module. If omitted, then the field type must be a caddy.ModuleMap
or []caddy.ModuleMap
, where the map key is the module name.
Loading guest modules
To load a guest module, call ctx.LoadModule()
during the provision phase:
// Provision sets up g and loads its gadget.
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
}
Note that the LoadModule()
call takes a pointer to the struct and the field name as a string. Weird, right? Why not just pass the struct field directly? It's because there are a few different ways to load modules depending on the layout of the config. This method signature allows Caddy to use reflection to figure out the best way to load the module and, most importantly, read its struct tags.
If a guest module must explicitly be set by the user, you should return an error if the Raw field is nil or empty before trying to load it.
Notice how the loaded module is type-asserted: g.Gadget = val.(Gadgeter)
- this is because the returned val
is a interface{}
type which is not very useful. However, we expect that all modules in the declared namespace (foo.gizmo.gadgets
from the struct tag in our example) implement the Gadgeter
interface, so this type assertion is safe, and then we can use it!
If your host module defines a new namespace, be sure to document both that namespace and its Go type(s) for developers like we have done here.
Module Documentation
Register the module to make a new Caddy module show up in the module documentation and be available in http://caddyserver.com/download. The registration is available at http://caddyserver.com/account. Create a new account if you don't have one already and click on "Register package".
Complete Example
Let's suppose we want to write an HTTP handler module. This will be a contrived middleware for demonstration purposes which prints the visitor's IP address to a stream on every HTTP request.
We also want it to be configurable via the Caddyfile, because most people prefer to use the Caddyfile in non-automated situations. We do this by registering a Caddyfile handler directive, which is a kind of directive that can add a handler to the HTTP route. We also implement the caddyfile.Unmarshaler
interface. By adding these few lines of code, this module can be configured with the Caddyfile! For example: visitor_ip stdout
.
Here is the code for such a module, with explanatory comments:
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 implements an HTTP handler that writes the
// visitor's IP address to a file or stream.
type Middleware struct {
// The file or stream to write to. Can be "stdout"
// or "stderr".
Output string `json:"output,omitempty"`
w io.Writer
}
// CaddyModule returns the Caddy module information.
func (Middleware) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "http.handlers.visitor_ip",
New: func() caddy.Module { return new(Middleware) },
}
}
// Provision implements 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("an output stream is required")
}
return nil
}
// Validate implements caddy.Validator.
func (m *Middleware) Validate() error {
if m.w == nil {
return fmt.Errorf("no writer")
}
return nil
}
// ServeHTTP implements 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 implements caddyfile.Unmarshaler.
func (m *Middleware) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
// require an argument
if !d.NextArg() {
return d.ArgErr()
}
// store the argument
m.Output = d.Val()
return nil
}
// parseCaddyfile unmarshals tokens from h into a new Middleware.
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
var m Middleware
err := m.UnmarshalCaddyfile(h.Dispenser)
return m, err
}
// Interface guards
var (
_ caddy.Provisioner = (*Middleware)(nil)
_ caddy.Validator = (*Middleware)(nil)
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
_ caddyfile.Unmarshaler = (*Middleware)(nil)
)