开发插件:服务器类型

英文原文:https://github.com/mholt/caddy/wiki/Writing-a-Plugin:-Server-Type


加入Caddy交流论坛和其他开发者一起分享。

Caddy附带一个HTTP服务器,但是你可以实现其他服务器类型并将它们插入Caddy中。其他类型的服务器可以是SSH、SFTP、TCP、内部使用的其他东西等等。

对于Caddy来说,服务器的概念是任何可以Listen()Serve()的东西。这意味着什么、如何运作都取决于你。你可以自由地发挥你的创造力去使用它。

如果你的服务器类型可以使用TLS,那么它应该利用Caddy的神奇TLS特性。我们将在本指南的最后描述如何做到这一点。

在一个高的层面上而言,使用caddy.RegisterServerType()函数实现一个服务器类型插件非常容易。传入服务器类型的名称,以及描述它的caddy.ServerType结构即可。

下面是一个(稍微简化了一些)HTTP服务器的示例:

import "github.com/mholt/caddy"

func init() {
    caddy.RegisterServerType("http", caddy.ServerType{
        Directives: directives,
        DefaultInput: func() caddy.Input {
            return caddy.CaddyfileInput{
                Contents:       []byte(fmt.Sprintf("%s:%s\nroot %s", Host, Port, Root)),
                ServerTypeName: "http",
            }
        },
        NewContext: newContext,
    })
}

如你所见,服务器类型包括名称(“http”)、指令列表、默认的Caddyfile输入(可选,但推荐)和上下文。现在我们将分别讨论这些。

指令

每个服务器类型都要定义它所识别的指令,以及它们应该以什么顺序执行。这只是一个字符串列表,其中每个字符串都是一个指令的名称:

var directives = []string{
    "dir1",
    "dir2",
    "etc",
}

在大多数服务器类型中,执行顺序非常重要,因此如果不在此列表中,则不会执行指令。

默认Caddyfile输入

这是可选的,但是强烈建议你不要默认使用空白的Caddyfile。这对你的服务器类型意味着什么取决于你自己。但是你可以返回一个caddy.CaddyfileInput实例以满足此功能。只有在需要时,Caddy才会调用你的函数。

上下文

Caddy试图避免尽可能多的全局状态。当Caddy加载、解析和执行Caddyfile时,它是在一个称为实例的作用域中执行的,这样一组服务器可以单独管理,多个服务器类型可以在同一个进程中启动。

每次Caddy经过这个启动阶段,都会创建一个新的caddy.instance。实例和Caddy将向你的服务器类型请求一个新的caddy.Context值,以便在不共享全局状态的情况下执行服务器的指令。阅读caddy.Contextgodoc,获取更多信息。

不应该使用服务器类型存储上下文列表;它们将与实例一起存储,并且可以从控制器访问。如果服务器稍后停止运行,那么保持对上下文的引用将防止它们被垃圾收集,这很可能造成内存泄漏。

最终,您的目标是使MakeServers()返回一个caddy.Server列表供Caddy使用。你怎么做完全取决于你自己。

例如:HTTP服务器使用其上下文类型保存站点配置的map。这个包还有一个公有函数GetConfig(c Controller),它为某个站点获取配置,传入指定的caddy.Controller。配置依次存储中间件列表等——所有设置站点所需的信息。每个指令的操作都调用GetConfig函数,帮助正确地设置站点。在调用MakeServers()时,只需将这些站点配置合并到服务器实例中并返回它们。(服务器实例实现了caddy.Server。)

服务器

caddy.Server的接口同时考虑了TCP和非TCP服务器。它有四个方法:两个针对TCP,两个针对UDP或其他:

type Server interface {
    TCPServer // Required if using TCP
    UDPServer // Required if using UDP or other
}

type TCPServer interface {
    Listen() (net.Listener, error)
    Serve(net.Listener) error
}

type UDPServer interface {
    ListenPacket() (net.PacketConn, error)
    ServePacket(net.PacketConn) error
}

如果你的服务器只使用TCP,*Packet()方法可能是空操作(即返回nil)。而非TCP服务器则与此相反。服务器还可以同时使用TCP和UDP,并实现所有四种方法。

一旦这些接口被实现,并且正确地从Caddyfile指令应用了配置,你的服务器类型就可以运行了。

自动TLS

可以使用TLS的服务器类型应该在将TLS添加到Caddy下载页面之前自动启用TLS。导入caddytls包以使用Caddy的魔术 TLS特性。乍一看,这似乎有点令人困惑,但一旦它起作用,你会喜欢它,并意识到它是值得的。

  • 你需要一种方式来为你的服务器实例存储Caddy TLS配置。通常,这只意味着向服务器的配置结构中添加一个字段。
  • 相同的服务器配置结构类型应该实现caddytls.ConfigHolder接口。这只是一些getter方法。
  • 在包的init()中调用RegisterConfigGetter(),这样caddytls包就知道在为服务器类型解析Caddyfile时如何请求配置。(如果给定控制器中还不存在配置,你的“配置getter”必须创建一个新的caddytls.Config
  • tls指令添加到服务器类型的指令列表中。通常它位于列表的最前面。

当你实例化实际的服务器值并需要tls.Config时,可以调用caddytls.MakeTLSConfig(tlsConfigs),其中tlsConfigs[]caddytls.Config。这个函数将一个Caddy的TLS配置列表转换为一个标准库的tls.Config。然后,你可以在对tls.NewListener()的调用中使用它。

最后,你通常希望在分析Caddyfile时启用TLS。例如,HTTP服务器在执行tls指令后立即配置HTTPS。你应该使用包的init()函数注册一个解析回调函数,该回调函数将遍历配置并配置TLS:

// 将"http"替换成你自己的服务器类型
caddy.RegisterParsingCallback("http", "tls", activateHTTPS)

这段代码在tls指令完成设置后执行activateHTTPS()函数。你的服务器类型应该有一个类似的函数,以一种合理的方式启用TLS。为了给你一个概念,HTTP服务器的activateHTTPS函数做了以下工作:

  1. 打印一条信息到标准输出,“激活隐私功能…”(Activating privacy features...)(如果操作出现;例如,caddy.Started() == false),因为这个过程可能需要几秒钟。
  2. 在所有应该完全管理的配置上将Managed字段设置为true
  3. 为每个配置调用 ObtainCert() (此方法仅在配置正确并将其Managed字段设置为true时才获得证书)。
  4. 通过将TLS配置的Enabled字段设置为true并调用caddytls.CacheManagedCertificate()来配置服务器结构以使用新获得的证书,而caddyls .cachemanagedcertificate()实际上是将证书加载到内存中以供使用。
  5. 调用[caddytls.SetDefaultTLSParams()]以确保所有必要的字段都有一个值。
  6. 调用caddytls.RenewManagedCertificates(true),以确保在必要时已将所有已加载到内存中的证书更新。

还有很多很多,但是你还可以看看HTTP服务器是如何工作的(<--这是一个永久链接,所以最新的代码可能更好)。

为了保持完美的前向保密,你应该在实例化服务器值时调用caddytls.RotateSessionTicketKeys(),传入TLS配置。请确保在服务器停止时关闭它返回的通道。

TLS的所有其他内容:续订、OCSP和其他维护都会为你进行,因为所有服务器类型都是相同的。所有这些步骤只是将你的服务器类型与Caddy的TLS包连接起来,这样它就知道如何完成它的工作。

彻底测试caddytls包的集成。一旦运行良好,让我们将你的服务器类型添加到下载页面!