Расширение Caddy
Caddy легко расширять благодаря модульной архитектуре. Большинство типов расширений Caddy (или плагинов) известны как модули, если они расширяют или подключаются к структуре конфигурации Caddy. Для ясности, модули Caddy отличаются от модулей Go (но они тоже являются модулями Go).
Предварительные требования:
- Базовое понимание архитектуры Caddy
- Знание языка Go
goxcaddy
Быстрый старт
Модуль Caddy — это любой именованный тип, который регистрируется как модуль Caddy при импорте своего пакета. Важно, что модуль всегда реализует интерфейс caddy.Module, который предоставляет его имя и функцию-конструктор.
В новом модуле Go вставьте шаблон в файл Go и настройте имя пакета, имя типа и идентификатор модуля Caddy:
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для предоставления идентификатора и конструктора - Имеют уникальное имя в соответствующем пространстве имён
- Обычно удовлетворяют некоторые интерфейсы, которые важны для модуля-хозяина в этом пространстве имён
Модули-хозяева (или родительские модули) — это модули, которые загружают/инициализируют другие модули. Обычно они определяют пространства имён для гостевых модулей.
Гостевые модули (или дочерние модули) — это модули, которые загружаются или инициализируются. Все модули являются гостевыми модулями.
Идентификаторы модулей
Каждый модуль Caddy имеет уникальный идентификатор, состоящий из пространства имён и имени:
- Полный идентификатор выглядит как
foo.bar.module_name - Пространство имён будет
foo.bar - Имя будет
module_name, которое должно быть уникальным в своём пространстве имён
Идентификаторы модулей должны использовать snake_case соглашение.
Пространства имён
Пространства имён похожи на классы, т.е. пространство имён определяет некоторую функциональность, которая является общей для всех модулей внутри него. Например, можно ожидать, что все модули в пространстве имён http.handlers являются обработчиками HTTP. Следует, что модуль-хозяин может привести тип гостевых модулей в этом пространстве имён от interface{} типов к более специфическому и полезному типу, такому как caddyhttp.MiddlewareHandler.
Гостевой модуль должен быть правильно размещён в пространстве имён, чтобы быть распознанным модулем-хозяином, потому что модули-хозяева будут запрашивать у Caddy модули в определённом пространстве имён для предоставления функциональности, необходимой модулю-хозяину. Например, если вы напишите модуль обработчика HTTP под названием gizmo, имя вашего модуля будет http.handlers.gizmo, потому что приложение http будет искать обработчики в пространстве имён http.handlers.
Другими словами, ожидается, что модули Caddy будут реализовывать определённые интерфейсы в зависимости от пространства имён модуля. С таким соглашением разработчики модулей могут говорить интуитивно, например: "Все модули в пространстве имён http.handlers являются обработчиками HTTP". Более технически, это обычно означает: "Все модули в пространстве имён http.handlers реализуют интерфейс caddyhttp.MiddlewareHandler". Поскольку этот набор методов известен, можно привести более конкретный тип и использовать его.
Просмотреть таблицу соответствия всех стандартных пространств имён Caddy их типам Go.
Пространства имён caddy и admin зарезервированы и не могут быть именами приложений.
Для написания модулей, которые подключаются к модулям-хозяевам сторонних разработчиков, обратитесь к документации этих модулей для получения информации о пространствах имён.
Имена
Имя внутри пространства имён важно и хорошо видно пользователям, но не особенно важно, если оно уникальное, краткое и имеет смысл для того, что делает.
Модули приложений
Приложения — это модули с пустым пространством имён и которые обычно становятся собственными пространствами имён верхнего уровня. Модули приложений реализуют интерфейс caddy.App.
Эти модули появляются в свойстве "apps" верхнего уровня конфигурации Caddy:
{
"apps": {}
}
Примеры приложений apps — это http и tls. Их пространство имён пустое.
Гостевые модули, написанные для этих приложений, должны быть в пространстве имён, полученном из имени приложения. Например, обработчики HTTP используют пространство имён http.handlers, а загрузчики сертификатов TLS — пространство имён tls.certificates.
Реализация модулей
Модуль может быть практически любым типом, но структуры — самые распространённые, потому что они могут хранить пользовательскую конфигурацию.
Конфигурация
Большинство модулей требуют некоторой конфигурации. Caddy позаботится об этом автоматически, если ваш тип совместим с JSON. Таким образом, если модуль является типом структуры, ему потребуются теги структуры для его полей, которые должны использовать snake_casing в соответствии с соглашениями Caddy:
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, чтобы помочь управлять глобальным состоянием во время загрузки модулей. Если ваш модуль слушает на сокете, используйте caddy.Listen*(), чтобы получить сокет, поддерживающий перекрывающееся использование.
Подготовка
Конфигурация модуля будет автоматически распакована в его значение (при загрузке конфигурации 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(), но модули не должны иметь циклических зависимостей. Другими словами, модуль, загруженный приложением http, не может зависеть от приложения tls, если модуль, загруженный приложением tls, зависит от приложения http. (Очень похоже на правила, запрещающие циклы импорта в Go.)
Кроме того, следует избегать выполнения дорогостоящих операций в Provision, так как подготовка выполняется даже если конфигурация только проверяется. На этапе подготовки не ожидайте, что модуль фактически будет использован.
Логи
См. как работает ведение журнала в Caddy. Если вашему модулю необходимы логи, не используйте log.Print*() из стандартной библиотеки Go. Другими словами, не используйте глобальный логгер Go. Caddy использует высокопроизводительные, гибкие, структурированные журналы с помощью zap.
Чтобы генерировать логи, получите логгер в методе подготовки вашего модуля:
func (g *Gizmo) Provision(ctx caddy.Context) error {
g.logger = ctx.Logger() // g.logger is a *zap.Logger
}
Затем вы можете генерировать структурированные, уровневые логи с помощью g.logger. Подробности см. в документации zap.
Валидация
Модули, которые хотели бы проверить свою конфигурацию, могут сделать это, удовлетворив (необязательный) интерфейс 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 узнать пространство имён и имя (составляющие полный идентификатор) модуля для загрузки. Он также используется для генерации документации.
Тег структуры имеет очень простой формат: 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 равно null или пусто, прежде чем пытаться загрузить его.
Обратите внимание, как загруженный модуль приводится к типу: g.Gadget = val.(Gadgeter) — это потому, что возвращаемый val имеет тип interface{}, который не очень полезен. Однако мы ожидаем, что все модули в объявленном пространстве имён (foo.gizmo.gadgets из тега структуры в нашем примере) реализуют интерфейс Gadgeter, поэтому приведение к типу безопасно, и мы можем его использовать!
Если ваш модуль-хост определяет новое пространство имён, убедитесь, что вы документируете как это пространство имён, так и его типы Go для разработчиков так, как мы это сделали здесь.
Документация модуля
Зарегистрируйте модуль, чтобы новый модуль Caddy появился в документации модуля и стал доступен в http://caddyserver.com/download. Регистрация доступна по адресу http://caddyserver.com/account. Создайте новую учётную запись, если у вас её нет, и нажмите «Зарегистрировать пакет».
Полный пример
Предположим, мы хотим написать модуль-обработчик HTTP. Это будет искусственный промежуточный обработчик для демонстрационных целей, который выводит IP-адрес посетителя в поток при каждом запросе HTTP.
Мы также хотим, чтобы он настраивался через 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)
)
© 2015-2025 Matthew Holt and The Caddy Authors
Licensed under the Apache License 2.0.
Caddy is a registered trademark of Stack Holdings GmbH.
https://caddyserver.com/docs/extending-caddy