擴充 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 is an example; put your own type here.
type Gizmo struct {
}
// CaddyModule returns the Caddy module information.
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"`
}
以這種方式使用結構體標籤將確保設定屬性在 Caddy 中的命名一致。
當模組初始化時,它已經填寫了其設定。在模組初始化後,也可以執行其他 配置 和 驗證 步驟。
模組生命週期
模組的生命週期始於它被主機模組載入時。會發生下列情況
- 呼叫
New()
以取得模組值的執行個體。 - 模組的設定會解組到該執行個體中。
- 如果模組是 caddy.Provisioner,則會呼叫
Provision()
方法。 - 如果模組是 caddy.Validator,則會呼叫
Validate()
方法。 - 此時,主機模組會收到載入的客體模組作為
interface{}
值,因此主機模組通常會將客體模組類型斷言為更有用的類型。查看主機模組的文件,以瞭解其名稱空間中客體模組的要求,例如需要實作哪些方法。 - 當模組不再需要時,且如果它是 caddy.CleanerUpper,則會呼叫
Cleanup()
方法。
請注意,您的模組的載入實例可能會在特定時間重疊!在組態變更期間,新的模組會在舊的模組停止之前啟動。務必小心使用全域狀態。使用 caddy.UsagePool 類型來協助管理模組載入中的全域狀態。如果您的模組在 socket 上監聽,請使用 caddy.Listen*()
來取得支援重疊使用情況的 socket。
配置
模組的組態會自動解析為其值。這表示,例如,結構欄位會自動填寫。
但是,如果您的模組需要額外的配置步驟,您可以實作(選擇性的)caddy.Provisioner 介面
// Provision sets up the module.
func (g *Gizmo) Provision(ctx caddy.Context) error {
// TODO: set up the module
return nil
}
這通常是主機模組載入其 guest/child 模組的地方,但它幾乎可用於任何地方。模組配置以任意順序執行。
模組可以透過呼叫 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 is a *zap.Logger
}
然後,您可以使用 g.logger
發出結構化、分層的記錄。請參閱 zap 的 godoc 以取得詳細資訊。
驗證
想要驗證其組態的模組可以透過滿足(選擇性的)caddy.Validator
介面來執行此操作
// Validate validates that the module has a usable config.
func (g Gizmo) Validate() error {
// TODO: validate the module's setup
return nil
}
驗證應為唯讀函式。它會在 Provision()
方法之後執行。
介面防護
Caddy 模組行為是隱含的,因為 Go 介面是隱含滿足的。只要將正確的方法新增到模組的類型中,即可建立或破壞模組的正確性。因此,輸入錯誤或方法簽章錯誤可能會導致意外(缺乏)行為。
幸運的是,您可以將一個簡單、無負擔、編譯時間的檢查新增到您的程式碼中,以確保您已新增正確的方法。這些稱為介面防護
var _ InterfaceName = (*YourType)(nil)
將 InterfaceName
替換為您打算滿足的介面,並將 YourType
替換為您的模組類型的名稱。
例如,靜態檔案伺服器等 HTTP 處理常式可能會滿足多個介面
// Interface guards
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:"-"`
}
第一個欄位(此範例中的 GadgetRaw
)是客體模組的原始、未提供 JSON 表單所在的位置。
第二個欄位(Gadget
)是最終的、已提供的值最終將儲存的位置。由於第二個欄位不是使用者介面,因此我們使用結構標籤將其從 JSON 中排除。(如果其他套件不需要它,您也可以取消匯出它,然後不需要結構標籤。)
Caddy 結構標籤
原始模組欄位上的 caddy
結構標籤有助於 Caddy 瞭解要載入的模組的命名空間和名稱(組成完整的 ID)。它也用於產生文件。
結構標籤的格式非常簡單:key1=val1 key2=val2 ...
對於模組欄位,結構標籤看起來像
`caddy:"namespace=foo.bar inline_key=baz"`
namespace=
部分是必需的。它定義要尋找模組的命名空間。
inline_key=
部分僅在模組的名稱會與模組本身內嵌時使用;這表示值是一個物件,其中一個金鑰是內嵌金鑰,其值是模組的名稱。如果省略,則欄位類型必須是 caddy.ModuleMap
或 []caddy.ModuleMap
,其中地圖金鑰是模組名稱。
載入客體模組
若要載入客體模組,請在提供階段呼叫 ctx.LoadModule()
// 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
}
請注意,LoadModule()
呼叫會將指標傳遞給結構,並將欄位名稱傳遞為字串。很奇怪,對吧?為什麼不直接傳遞結構欄位?這是因為根據組態的配置,有幾種不同的方式可以載入模組。這個方法簽章允許 Caddy 使用反射來找出載入模組的最佳方式,最重要的是,讀取其結構標籤。
如果訪客模組必須由使用者明確設定,則在嘗試載入之前,您應該傳回錯誤,如果 Raw 欄位為 nil 或空白。
請注意載入的模組如何進行類型斷言:g.Gadget = val.(Gadgeter)
- 這是因為傳回的 val
是 interface{}
類型,這並不是很實用。然而,我們預期已宣告命名空間(範例中的結構標籤為 foo.gizmo.gadgets
)中的所有模組都實作 Gadgeter
介面,因此這個類型斷言是安全的,然後我們可以使用它!
如果您的主機模組定義新的命名空間,請務必為開發人員記錄該命名空間及其 Go 類型,就像我們在此處所做的一樣。
模組文件
註冊模組以使新的 Caddy 模組顯示在模組文件中,並在 https://caddy.dev.org.tw/download 中提供。註冊可在 https://caddy.dev.org.tw/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 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)
)