Working With Caddy

Introduction

In our previous post, we looked at how we can build a HTTPS server in Go from scratch. Most of the time, this is not always the quickest approach. Adding features such as logging, metrics, authentication, access control, and encryption can eat up into a lot of development time as they are daunting and hard to get right. Instead, you might find it easier and more convenient to use an existing web server to host your web services. This is where Caddy comes in. Caddy helps you spend your time writing web services and less about worrying how to serve your application. We’ll look at how we can extend Caddy’s functionality using modules in this post and how we can use these modules to serve our application and proxy requests to our web services in another post. So, what’s Caddy?

Caddy

Caddy is a contemporary web server that focuses on security, performance, and ease of use. It’s key features is that it offers TLS certificate management straight-out-of-the-box allowing you to easily implement HTTPS. Since it’s built in Go, it takes advantage of its concurrency model to handle huge amounts of traffic making your services stable.

Downloading and Installing Caddy

I’m running Caddy on Windows 10, but it comes with support for several operating systems. Head over to their install documentation page to find details on what works for you.

If you don’t find a suitable binary for your OS and/or architecture, you can compile it from its source code. A Go installation is required for this to work.

$ git clone https://github.com/caddyserver/caddy.git

cd caddy/cmd/caddy

go build

You should see the following output:

caddy

After the build is complete, make sure you add the caddy binary in your PATH.

Modules and Adapters

Caddy’s architecture is modular. This allows you to extend its functionality by writing your own modules and config adapters. In this section, we’ll write our own configuration adapter and a middleware module which we’ll then use as a base for our proxy in the web service we’ll build.

Configuration Adapter

Caddy uses the Tom’s Obvious, Minimal Language (TOML) file format to store its configuration files. The reason we don’t use JSON even though it could also work, is because it lacks support for comments and multiline strings; two factors which make it undesirable in terms of readability.

In your project directory, run these commands:

$ mkdir toml-adapter

cd toml-adapter

go mod init github.com/[username]/toml-adapter

Create a file called toml.go in the same directory and add the following contents:

package toml

import (
"encoding/json"

"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/pelletier/go-toml"
)

func init() {
    caddyconfig.RegisterAdapter("toml", Adapter{})
}

type Adapter struct{}

func (a Adapter) Adapt(body []byte, _ map[string]interface{}) ([]byte, []caddyconfig.Warning, error) {
    tree, err := toml.LoadBytes(body)
    if err != nil {
	    return nil, nil, err
    }
    b, err := json.Marshal(tree.ToMap())
    return b, nil, err
}

We use the package by Thomas Pelletier to parse our config file contents. This saves us a lot of code. You then convert the parsed TOML into a map then marshal the map into JSON.

Tidy up the module:

go mod tidy

Middleware Module

In Go, middleware’s are functions that accept an http.Handler and returns an http.Handler:

func (http.Handler) http.Handler

An http.Handler describes an object with a ServeHTTP method that accepts an http.RequestWriter and an http.Request:

type Handler interface {
    ServeHTTP(http.ResponseWriter, *http.Request)
}

You could then use a chain of middleware on your handler that implements the http.Handler interface as follows:

h := middleware1(middleware2(middleware3(handler)))

Caddy’s middleware doesn’t use the same pattern as the net/http package. Its equivalent is the caddyhttp.Handler interface:

type Handler interface {
    ServeHTTP(http.ResponseWriter, r *http.Request) error
}

The only difference between the two is that Caddy’s ServeHTTP method returns an error interface.

In your project directory, run these commands:

mkdir auth-prefix

cd auth-prefix

go mod init github.com/[username]/auth-prefix

Create a file and name it auth_prefix.go. Add the following content:

func init() {
caddy.RegisterModule(AuthPrefix{})
}

type AuthPrefix struct {
Prefix string `json:"prefix,omitempty"`
logger *zap.Logger
}

func (AuthPrefix) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
	ID: "http.handlers.auth_prefix",
	New: func() caddy.Module { return new(AuthPrefix) },
}
}

func (p *AuthPrefix) Provision(ctx caddy.Context) error {
p.logger = ctx.Logger(p)
return nil
}

func (p *AuthPrefix) Validate() error {
if p.Prefix == "" {
	p.Prefix = "."
}
return nil
}

func (p AuthPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
for _, part := range strings.Split(r.URL.Path, "/") {
	if strings.HasPrefix(part, p.Prefix) {
		http.Error(w, "Not Found", http.StatusNotFound)
		if p.logger != nil {
			p.logger.Debug(fmt.Sprintf(
				"authorized prefix: %q in %s", part, r.URL.Path))
			}
			return nil
		}
	}
	return next.ServeHTTP(w, r)
}

var (
	_ caddy.Provisioner = (*AuthPrefix)(nil)
	_ caddy.Validator = (*AuthPrefix)(nil)
	_ caddyhttp.MiddlewareHandler = (*AuthPrefix)(nil)
)

You first register your module with Caddy upon initialization then storing it in an AuthPrefix struct then assigning it a struct tag that will be unmarshalled by matching incoming keys to struct tags. The logger field in the struct is for logging events, if necessary. Upon module initialization, you add the CaddyModule method to return information to Caddy about it. Caddy also requires an ID for each module and function to create a new instance of the module.

caddy.Provisioner, caddy.Context and caddy.Validator are interfaces that ensure all the required settings have been unmarshaled from the configuration into your module. The last part of the code are there to ensure that your code implements the required interfaces and guard against compilation failures in future whenever you change your code.

Tidy up your module’s dependencies and publish it:

go mod tidy

Injecting the Module

After publishing your modules, you can now include them at runtime build. Let’s do that:

$ mkdir caddy

$ cd caddy

Create a main.go file and add this boilerplate code:

package main

import (
    cmd "github.com/caddyserver/caddy/v2/cmd"
    _ "github.com/caddyserver/caddy/v2/modules/standard"

    _ "github.com/rabbice/toml-adapter"
    _ "github.com/rabbice/auth-prefix"
)

func main() {
    cmd.Main()
}

Save the file and run these commands in your terminal:

$ go mod init caddy

go build

After that, you should have a binary named caddy in your directory. You can verify that it has your custom imports by looking for them in the caddy binary’s list of modules. Run the following commands:

Linux and macOS: ./caddy list-modules | grep "toml\|auth_prefix"

Output:

caddy.adapters.toml
http.handlers.auth_prefix

Windows: caddy list-modules | findstr "toml auth_prefix"

Same output as above.

The caddy binary you built can read its configuration from TOML files and deny clients access to resources whose path includes a given prefix. That’s the essence of the middleware.

In our next post we shall look at how we can use what we’ve built here to proxy requests to our backend web service (which we shall also build).

Thank you for reading, until next time.