Skip to content

v2: Writing a Module

Matt Holt edited this page Sep 17, 2019 · 5 revisions

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 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

Caddy extensions are called "modules" (distinct from Go modules), or "plugins".

Modules register themselves with Caddy when they are imported. Here's a template you can copy & paste:

package mymodule

import "github.com/caddyserver/caddy/v2"

func init() {
	err := caddy.RegisterModule(Gizmo{})
	if err != nil {
		log.Fatal(err)
	}
}

// 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{
		Name: "foo.gizmo",
		New: func() caddy.Module { return new(Gizmo) },
	}
}

A module can then be "plugged in" by adding an import to Caddy's main package:

package main

import _ "github.com/example/mymodule"

Module Requirements

Caddy modules:

  1. Implement the caddy.Module interface to provide a name and constructor
  2. Have a unique name in the proper namespace
  3. Satisfy the host module's defined interface for that namespace

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 would be 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:

{
	"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. App modules implement the caddy.App interface. See below for a table of namespaces and their corresponding interfaces.

Namespaces

You must properly namespace your guest module in order for it to be recognized by a 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 and admin namespaces are reserved.

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, the New function is called to obtain a new instance of type interface{}. Typically, 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.

Configuration

A module should usually 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:

type Gizmo struct {
	MyField string `json:"my_field,omitempty"`
	Number  int    `json:"number,omitempty"`
}

This will ensure that config properties are consisently named across all of Caddy 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 and verification steps when your module is loaded.

Table of Namespaces and Interfaces

Kind of module Namespace Interface
Caddy app caddy.App
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 instance.
  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; 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, and if it is a caddy.CleanerUpper, the Cleanup() method is called.

Note that multiple loaded instances of your module may overlap at a given time. During config reloads, modules are started before the old ones are stopped. Be sure to use global state carefully. Use the caddy.UsagePool type to help manage global state across module loads.

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:

// 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:

// 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:

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:

// Interface guards
var (
	_ caddy.Provisioner              = (*FileServer)(nil)
	_ caddyhttp.MiddlewareHandler    = (*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.

Host Modules

As explained above, a host module is a module which can load its own guest modules. In other words, is configuration/behavior is itself modular or pluggable.

A host module will need to load and use its guest modules. Caddy provides facilities for this. This section describes how to do this in a conventional way.

First, host modules need a way to receive the guest modules' configuration. This is typically done by adding json.RawMessage fields to the struct, and then using the Provision method to load them into a non-JSON type. For example, if your module Gizmo has a Gadget guest module that implements a Gadgeter interface, you would add two fields, GadgetRaw and Gadget:

	GadgetRaw  json.RawMessage `json:"gadget,omitempty"`

	// the decoded value of the guest module will be
	// stored here, but it doesn't get used for JSON
	// so make sure to exclude it with "-"
	Gadget Gadgeter `json:"-"`

The configuration for Gadget would come in on the GadgetRaw field and then your Provision() method would be used to decode the config and load the instance of the Gadget.

There are two functions for loading guest modules: LoadModule() and LoadModuleInline().

  • LoadModule() loads a module from its json.RawMessage when you already know the module's name. This is often the case when the module name is stored outside of the module's configuration; like as a map key, where the value is the module's config.
  • LoadModuleInline() is used when you have only a json.RawMessage and you do not know the module's name, because its name is stored within the JSON. In other words, its module name is found inline with its configuration. To use this function, you provide the namespace as well as the name of the property that contains the module's name in that namespace.

Here's an example of using LoadModuleInline() to get a Gadgeter for our Gizmo module:

// Provision sets up g and loads its gadget.
func (g *Gizmo) Provision(ctx caddy.Context) error {
	if g.GadgetRaw != nil {
		val, err := ctx.LoadModuleInline("gadgeter", "foo.gizmo.gadgets", g.GadgetRaw)
		if err != nil {
			return fmt.Errorf("loading gadget module: %v", err)
		}
		g.Gadget = val.(Gadgeter)
		g.GadgetRaw = nil // allow GC to deallocate
	}
	return nil
}

If the guest module is a required field, you should return an error if the Raw field is nil or empty.

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. We do this by registering a Caddyfile handler directive, which is a kind of directive that can add a handler to the HTTP route. We also implement the caddyfile.Unmarshaler interface. By adding these few lines of code, this module can be configured with the Caddyfile! For example: visitor_ip stdout.

Here is the code for such a module, with explanatory comments:

package visitorip

import (
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/caddyserver/caddy"
	"github.com/caddyserver/caddy/config/caddyfile"
	"github.com/caddyserver/caddy/config/httpcaddyfile"
	"github.com/caddyserver/caddy/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{
		Name: "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 {
	for d.Next() {
		if !d.Args(&m.Output) {
			return d.ArgErr()
		}
	}
	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)
)
You can’t perform that action at this time.