擴展 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
命名空間。
模組實作
模組幾乎可以是任何類型,但結構是最常見的,因為它們可以保存使用者配置。
配置
大多數模組都需要一些配置。 Caddy 會自動處理此問題,只要您的類型與 JSON 相容。 因此,如果模組是結構類型,則需要在其欄位上使用結構標籤,結構標籤應根據 Caddy 慣例使用 snake_casing
type Gizmo struct {
MyField string `json:"my_field,omitempty"`
Number int `json:"number,omitempty"`
}
在結構標籤中使用 omitempty
選項將從 JSON 輸出中省略該欄位(如果它是其類型的零值)。 這對於在封送處理時保持 JSON 配置的簡潔明瞭非常有用(例如,從 Caddyfile 適應到 JSON)。
當模組初始化時,它將已填寫其配置。 還可以在模組初始化後執行其他佈建和驗證步驟。
模組生命週期
模組的生命在它被主機模組載入時開始。 接下來會發生:
New()
被呼叫以取得模組值的實例。- 模組的配置被解組到該實例中。
- 如果模組是
caddy.Provisioner
,則呼叫Provision()
方法。 - 如果模組是
caddy.Validator
,則呼叫Validate()
方法。 - 此時,主機模組會獲得載入的客模組作為
interface{}
值,因此主機模組通常會將客模組類型斷言為更有用的類型。 查看主機模組的文檔,以了解其命名空間中客模組的要求,例如需要實現哪些方法。 - 當不再需要模組時,如果它是
caddy.CleanerUpper
,則呼叫Cleanup()
方法。
請注意,模組的多個載入實例可能在給定時間重疊! 在配置變更期間,新模組會在舊模組停止之前啟動。 請務必謹慎使用全域狀態。 使用 caddy.UsagePool
類型來幫助管理跨模組載入的全域狀態。 如果您的模組監聽 socket,請使用 caddy.Listen*()
取得支援重疊使用的 socket。
佈建
模組的配置將自動解組到其值中(載入 JSON 配置時)。 這表示,例如,結構欄位將為您填寫。
但是,如果您的模組需要額外的佈建步驟,您可以實現(可選)caddy.Provisioner
介面
// Provision sets up the module.
func (g *Gizmo) Provision(ctx caddy.Context) error {
// TODO: set up the module
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 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
}
Validate 應為唯讀函式。 它在 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)
)