_Thank you so much for this project. I started playing with it over the weekend and decided to build custom modules and directives to simulate failures. It's made me excited to write code in my personal time all over again!_
Background
I want to build a suite of modules that plug in to Caddy and allow it to inject failures for the purpose of testing failure recovery. For example, I would deploy a Caddy reverse proxy in front of some API and add directives to delay the response or rate limit agressively.
Question
The current problem I'm facing is that I've written a module that implements caddy.ListenerWrapper and injects a long pause before accepting connections. I want to have it controlled by a directive like so miasma_tarpit 10s. I can't seem to understand how to use httpcaddyfile.RegisterDirective to register the directive. In particular, I'm not understanding what sort of []ConfigValue I should be returning. I'm not sure what value for Class I should pass. Can you help understand what I'm doing wrong?
Code
The module:
package tarpit
import (
"context"
"fmt"
"math/rand"
"net"
"time"
"github.com/caddyserver/caddy/v2"
"go.uber.org/zap"
)
func init() {
caddy.RegisterModule(Tarpit{})
httpcaddyfile.RegisterDirective("miasma_tarpit", parseCaddyfile)
}
type tarpitKey int
// Tarpit is a Caddy module that alters response times
type Tarpit struct {
rootContext context.Context
random *rand.Rand
logger *zap.Logger
// TODO
Delay time.Duration `json:"delay,omitempty"`
}
var (
_ caddy.Module = (*Tarpit)(nil)
_ caddy.Provisioner = (*Tarpit)(nil)
_ caddy.Validator = (*Tarpit)(nil)
_ caddy.ListenerWrapper = (*Tarpit)(nil)
)
// CaddyModule returns the Caddy module information.
func (Tarpit) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.listeners.miasma_tarpit",
New: func() caddy.Module { return new(Tarpit) },
}
}
// Provision initializes the internal state of the Tarpit module
func (t *Tarpit) Provision(ctx caddy.Context) error {
fmt.Println("PROVISION")
t.rootContext = context.WithValue(ctx, tarpitKey(0), nil)
t.logger = ctx.Logger(t)
return nil
}
// Validate ensures the configuration for the Tarpit module is valid
func (t *Tarpit) Validate() error {
fmt.Println("VALIDATE")
if t.logger == nil {
return fmt.Errorf("logger cannot be nil")
}
if t.Delay <= 0 {
return fmt.Errorf("tarpit delay must be great than zero: %v", t.Delay)
}
return nil
}
func (t *Tarpit) WrapListener(listener net.Listener) net.Listener {
return &tarpitListener{
rootContext: t.rootContext,
underlying: listener,
delay: t.Delay,
}
}
type tarpitListener struct {
rootContext context.Context
underlying net.Listener
delay time.Duration
}
func (l *tarpitListener) Accept() (net.Conn, error) {
select {
case <-l.rootContext.Done():
return nil, context.Canceled
case <-time.After(l.delay):
return l.underlying.Accept()
}
}
func (l *tarpitListener) Close() error {
return l.underlying.Close()
}
func (l *tarpitListener) Addr() net.Addr {
return l.underlying.Addr()
}
func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
var tarpit Tarpit
if !h.Next() {
return nil, h.ArgErr()
}
_, err := h.ExtractMatcherSet()
if err != nil {
return nil, err
}
if !h.Next() {
return nil, h.ArgErr()
}
args := h.RemainingArgs()
fmt.Println(args)
if len(args) != 1 {
return nil, h.ArgErr()
}
delay, err := time.ParseDuration(args[0])
if err != nil {
return nil, err
}
tarpit.Delay = delay
// what do I return???
}
Ah yes, this is not very well-documented yet. I'm actually planning on expanding the "Extending Caddy" part of the docs as soon as I get a chance!
Basically, the config values you return there each belong to some class. They get dumped into a pile, and then later when the Caddyfile adapter is assembling the JSON config, it picks values out from a relevant class at each part of the config.
Currently, I don't think the Caddyfile adapter has a class for listener wrappers, so we'd need to add that. Not a big deal, just needs a pull request :) The class name could be listener_wrapper or something like that. Then the Caddyfile adapter would have to—when it is building the HTTP server—just look in the pile for config values of class listener_wrapper and JSON-encode them.
Does that make sense? Sounds like a cool project!
Specifically, one of the places in the code that would need to be modified is here:
https://github.com/caddyserver/caddy/blob/master/caddyconfig/httpcaddyfile/httptype.go#L328-L549
Look for the parts that use sblock.pile, you'd probably need to add a sblock.pile["listener_wrapper"] pile
@mholt @francislavoie, thanks for the tips! Makes sense to me. I'm having a go at a PR right now!
Most helpful comment
@mholt @francislavoie, thanks for the tips! Makes sense to me. I'm having a go at a PR right now!