mirror of
https://github.com/caddyserver/caddy.git
synced 2025-07-09 03:04:57 -04:00
Created v2: Writing a Module (markdown)
parent
c105581f7e
commit
eabb50efb5
301
v2:-Writing-a-Module.md
Normal file
301
v2:-Writing-a-Module.md
Normal file
@ -0,0 +1,301 @@
|
||||
Extending Caddy 2 is easy because of Caddy's modular architecture.
|
||||
|
||||
NOTE: Caddy 2 is not yet a stable 2.0 release, so breaking changes are still possible. We will try to minimize them but please stay attentive just in case. :)
|
||||
|
||||
If you are writing a Caddy 2 module, please let us know in our [forum](https://caddy.community) or post an issue, especially if you encounter difficulties. We want to make this as easy and flexible as possible, so let us know what you're making and how it goes!
|
||||
|
||||
# Contents
|
||||
|
||||
- [Module Basics](#module-basics)
|
||||
- [Module Requirements](#module-requirements)
|
||||
- [Module Names](#module-names)
|
||||
- [Module Implementation](#module-implementation)
|
||||
- [Configuration](#configuration)
|
||||
- [Table of Namespaces and Interfaces](#table-of-namespaces-and-interfaces)
|
||||
- [Module Lifecycle](#module-lifecycle)
|
||||
- [Provisioning](#provisioning)
|
||||
- [Validating](#validating)
|
||||
- [Interface guards](#interface-guards)
|
||||
- [Complete Example](#complete-example)
|
||||
|
||||
|
||||
# Module Basics
|
||||
|
||||
Caddy extensions are called "modules" (distinct from Go modules), or "plugins".
|
||||
|
||||
Modules register themselves with Caddy when they are imported:
|
||||
|
||||
```go
|
||||
package mymodule
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(caddy.Module{
|
||||
Name: "foo.bar.gizmo",
|
||||
New: func() interface{} { return new(Gizmo) },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
A module can then be "plugged in" by adding an import to Caddy's `main` package:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import _ "github.com/example/mymodule"
|
||||
```
|
||||
|
||||
# Module Requirements
|
||||
|
||||
Modules work successfully when they:
|
||||
|
||||
1. Have a unique name in the proper namespace
|
||||
2. Satisfy the host module's defined interface for that namespace
|
||||
|
||||
You can see how registering a module correlates to these two requirements:
|
||||
|
||||
```go
|
||||
caddy.RegisterModule(caddy.Module{
|
||||
// name must be properly namespaced ("foo.bar")
|
||||
Name: "foo.bar.gizmo",
|
||||
|
||||
// the returned value must satisfy an interface
|
||||
// expected for modules in the "foo.bar" namespace;
|
||||
// if the value is not a pointer, it will be
|
||||
// converted to one using reflection
|
||||
New: func() interface{} { return new(Gizmo) },
|
||||
})
|
||||
```
|
||||
|
||||
We will show you how to satisfy these requirements in the next sections. (It's really quite easy!)
|
||||
|
||||
# Module Names
|
||||
|
||||
Each Caddy module has a unique name, consisting of a namespace and ID:
|
||||
|
||||
- A name looks like `a.b.c.module_id`
|
||||
- The namespace would be `a.b.c`
|
||||
- The module ID is `module_id` which must be unique in its namespace
|
||||
|
||||
_Host modules_ (or _parent modules_) are modules which load/initialize other modules. They typically define namespaces for guest modules.
|
||||
|
||||
_Guest modules_ (or _child modules_) are modules which get loaded or initialized. All modules are guest modules.
|
||||
|
||||
Module names should use `snake_case` convention.
|
||||
|
||||
## Apps
|
||||
|
||||
Apps are modules which define their own top-level namespace, and which appear in the `"apps"` property of the top-level of Caddy's config:
|
||||
|
||||
```json
|
||||
{
|
||||
"apps": {}
|
||||
}
|
||||
```
|
||||
|
||||
Example apps are `http` and `tls`. Their module name and namespace are the same. Guest modules use a namespace inherited from their host module. For example, HTTP handlers use the `http.handlers` namespace and TLS certificate loaders use the `tls.certificates` namespace.
|
||||
|
||||
## Namespaces
|
||||
|
||||
You must properly namespace your module in order for it to be recognized by the host module. You can find a table of standard namespaces below.
|
||||
|
||||
For example, if you were to write an HTTP handler module called `gizmo`, your module's name would be `http.handlers.gizmo`, because the `http` app will look for handlers in the `http.handlers` namespace.
|
||||
|
||||
The `caddy` namespace is reserved for modules specific to Caddy's core configuration.
|
||||
|
||||
To write modules which plug into 3rd-party host modules, consult those modules for their namespace documentation.
|
||||
|
||||
# Module Implementation
|
||||
|
||||
Typically, a namespace is associated with at least one interface the module must implement in order to be used. When a module is loaded, its `New` function is called which returns an empty interface. Usually, this is then type-asserted to be a specific interface so that it can be used. If your module does not implement the interface for that namespace, it will panic or error. However, how guest modules are loaded can vary per host module.
|
||||
|
||||
## Configuration
|
||||
|
||||
A module should be configurable. Its configuration will automatically be unmarshaled into its type when it is loaded. If your module is a struct type, you will need to use JSON struct tags on your fields and use `snake_casing`:
|
||||
|
||||
```go
|
||||
type Gizmo struct {
|
||||
MyField string `json:"my_field,omitempty"`
|
||||
Number int `json:"number,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
This will ensure that config properties are consisently named and that users can configure your module easily.
|
||||
|
||||
You can expect that instances of your module are filled out with any configuration they were loaded with. It is also possible to perform additional [provisioning](#provisioning) and [verification](#validating) steps when your module is loaded.
|
||||
|
||||
## Table of Namespaces and Interfaces
|
||||
|
||||
| Kind of module | Namespace | Interface |
|
||||
| -------------- | --------- | --------- |
|
||||
| HTTP handler | `http.handlers` | caddyhttp.MiddlewareHandler |
|
||||
| HTTP encoder | `http.encoders` | caddyhttp.Encoder |
|
||||
| HTTP request matcher | `http.matchers` | caddyhttp.RequestMatcher |
|
||||
| TLS certificate management | `tls.management` | (TODO) |
|
||||
| TLS handshake matcher | `tls.handshake_match` | caddytls.ConnectionMatcher |
|
||||
| TLS session tickets | `tls.stek` | caddytls.STEKProvider |
|
||||
| Caddy storage | `caddy.storage` | caddy.StorageConverter |
|
||||
|
||||
For example, HTTP handlers satisfy the `caddyhttp.MiddlewareHandler` interface.
|
||||
|
||||
|
||||
## Module Lifecycle
|
||||
|
||||
When your module is loaded by a host module, the following happens:
|
||||
|
||||
1. `New` is called to get an instance of the module's value.
|
||||
2. The module's configuration is unmarshaled into that value.
|
||||
3. If the module is a `caddy.Provisioner`, the `Provision()` method is called.
|
||||
4. If the module is a `caddy.Validator`, the `Validate()` method is called.
|
||||
5. At this point, the module value will probably be type-asserted by the host module into the interface it is expected to implement so that it can be used. However, this can vary per host module. It could be that some modules may optionally satisfy interfaces. Check the host module's documentation to know for sure.
|
||||
6. When a module is no longer needed, if it is a `caddy.CleanerUpper`, the `Cleanup()` method is called.
|
||||
|
||||
Note that multiple, loaded instances of your module may be overlap. During config reloads, modules are started before the old ones are stopped. Be sure to use global state carefully.
|
||||
|
||||
## Provisioning
|
||||
|
||||
A module's configuration will be unmarshaled into its value for you.
|
||||
|
||||
However, if your module requires additional provisioning steps, you can implement the `caddy.Provisioner` interface:
|
||||
|
||||
```go
|
||||
// Provision sets up the module.
|
||||
func (g *Gizmo) Provision(ctx caddy.Context) error {
|
||||
// TODO: set up the module
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
This is typically where host modules will load their guest/child modules, but it can be used for pretty much anything.
|
||||
|
||||
App modules which use this method MUST NOT depend on other apps in the provision phase, since provisioning of apps is done in an arbitrary order.
|
||||
|
||||
|
||||
## Validating
|
||||
|
||||
Modules which would like to validate their configuration may do so by satisfying the `caddy.Validator` interface:
|
||||
|
||||
```go
|
||||
// Validate validates that the module has a usable config.
|
||||
func (g Gizmo) Validate() error {
|
||||
// TODO: validate the module's setup
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Validate should be a read-only function. It is run during the provisioning phase, after the `Provision()` method (if any).
|
||||
|
||||
|
||||
## Interface guards
|
||||
|
||||
Caddy module behavior is implicit because Go interfaces are satisfied implicitly. Simply adding the right methods to your module's type is all it takes to make or break your module's correctness.
|
||||
|
||||
Fortunately, there is an easy, no-overhead, compile-time check you can add to your code to ensure you've added the right methods. These are called interface guards:
|
||||
|
||||
```go
|
||||
var _ InterfaceType = (*YourType)(nil)
|
||||
```
|
||||
|
||||
Replace `InterfaceType` with the interface you intend to satisfy, and `YourType` with the name of your module's type.
|
||||
|
||||
For example, an HTTP handler such as the static file server might satisfy multiple interfaces:
|
||||
|
||||
```go
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*FileServer)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*FileServer)(nil)
|
||||
_ httpcaddyfile.HandlerDirective = (*FileServer)(nil)
|
||||
)
|
||||
```
|
||||
|
||||
This prevents the program from compiling if `*FileServer` does not satisfy those interfaces.
|
||||
|
||||
Without interface guards, confusing bugs can slip in. For example, if your module must provision itself before being used but your `Provision()` method has a mistake (e.g. misspelled or wrong signature), provisioning will never happen, leading to head-scratching. Interface guards are super easy and can prevent that.
|
||||
|
||||
|
||||
# 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. The two methods satisfying `httpcaddyfile.HandlerDirective` allow this module to be used with simple configuration in the Caddyfile, for example: `visitor_ip stdout`.
|
||||
|
||||
Here is the code for such a module, with explanatory comments:
|
||||
|
||||
```go
|
||||
package visitorip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/caddyserver/caddy"
|
||||
"github.com/caddyserver/caddy/modules/caddyhttp"
|
||||
"github.com/caddyserver/caddy/config/caddyfile"
|
||||
"github.com/caddyserver/caddy/config/httpcaddyfile"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(caddy.Module{
|
||||
Name: "http.handlers.visitor_ip",
|
||||
New: func() interface{} { return new(Middleware) },
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if !d.Args(&m.Output) {
|
||||
return fmt.Errorf("an output stream is required: stdout or stderr")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bucket returns the HTTP directive bucket for this handler.
|
||||
func (m Middleware) Bucket() int { return 1 }
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ caddy.Provisioner = (*Middleware)(nil)
|
||||
_ caddy.Validator = (*Middleware)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Middleware)(nil)
|
||||
_ httpcaddyfile.HandlerDirective = (*Middleware)(nil)
|
||||
)
|
||||
```
|
Loading…
x
Reference in New Issue
Block a user