@marcel91200 / caddy Mirror · read-only / commits / d4d8bbcf
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 14:15 UTC

events: Implement event system (#4912)

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Francis Lavoie committed Aug 31, 2022 at 17:01 UTC d4d8bbcfc64d1194079cae35697709f6d267d02f
13 files changed +569 -34
context.go
+52 -16
@@ -37,9 +37,10 @@ import (
37 // not actually need to do this).
38 type Context struct {
39 context.Context
40 - moduleInstances map[string][]any
40 + moduleInstances map[string][]Module
41 cfg *Config
42 cleanupFuncs []func()
43 + ancestry []Module
44 }
45
46 // NewContext provides a new context derived from the given
@@ -51,7 +52,7 @@ type Context struct {
52 // modules which are loaded will be properly unloaded.
53 // See standard library context package's documentation.
54 func NewContext(ctx Context) (Context, context.CancelFunc) {
54 - newCtx := Context{moduleInstances: make(map[string][]any), cfg: ctx.cfg}
55 + newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg}
56 c, cancel := context.WithCancel(ctx.Context)
57 wrappedCancel := func() {
58 cancel()
@@ -90,15 +91,15 @@ func (ctx *Context) OnCancel(f func()) {
91 // ModuleMap may be used in place of map[string]json.RawMessage. The return value's
92 // underlying type mirrors the input field's type:
93 //
93 -// json.RawMessage => any
94 -// []json.RawMessage => []any
95 -// [][]json.RawMessage => [][]any
96 -// map[string]json.RawMessage => map[string]any
97 -// []map[string]json.RawMessage => []map[string]any
94 +// json.RawMessage => any
95 +// []json.RawMessage => []any
96 +// [][]json.RawMessage => [][]any
97 +// map[string]json.RawMessage => map[string]any
98 +// []map[string]json.RawMessage => []map[string]any
99 //
100 // The field must have a "caddy" struct tag in this format:
101 //
101 -// caddy:"key1=val1 key2=val2"
102 +// caddy:"key1=val1 key2=val2"
103 //
104 // To load modules, a "namespace" key is required. For example, to load modules
105 // in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the
@@ -115,7 +116,7 @@ func (ctx *Context) OnCancel(f func()) {
116 // meaning the key containing the module's name that is defined inline with the module
117 // itself. You must specify the inline key in a struct tag, along with the namespace:
118 //
118 -// caddy:"namespace=http.handlers inline_key=handler"
119 +// caddy:"namespace=http.handlers inline_key=handler"
120 //
121 // This will look for a key/value pair like `"handler": "..."` in the json.RawMessage
122 // in order to know the module name.
@@ -301,17 +302,17 @@ func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[strin
302 // like from embedded scripts, etc.
303 func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error) {
304 modulesMu.RLock()
304 - mod, ok := modules[id]
305 + modInfo, ok := modules[id]
306 modulesMu.RUnlock()
307 if !ok {
308 return nil, fmt.Errorf("unknown module: %s", id)
309 }
310
310 - if mod.New == nil {
311 - return nil, fmt.Errorf("module '%s' has no constructor", mod.ID)
311 + if modInfo.New == nil {
312 + return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID)
313 }
314
314 - val := mod.New().(any)
315 + val := modInfo.New()
316
317 // value must be a pointer for unmarshaling into concrete type, even if
318 // the module's concrete type is a slice or map; New() *should* return
@@ -327,7 +328,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
328 if len(rawMsg) > 0 {
329 err := strictUnmarshalJSON(rawMsg, &val)
330 if err != nil {
330 - return nil, fmt.Errorf("decoding module config: %s: %v", mod, err)
331 + return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err)
332 }
333 }
334
@@ -340,6 +341,8 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
341 return nil, fmt.Errorf("module value cannot be null")
342 }
343
344 + ctx.ancestry = append(ctx.ancestry, val)
345 +
346 if prov, ok := val.(Provisioner); ok {
347 err := prov.Provision(ctx)
348 if err != nil {
@@ -351,7 +354,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
354 err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
355 }
356 }
354 - return nil, fmt.Errorf("provision %s: %v", mod, err)
357 + return nil, fmt.Errorf("provision %s: %v", modInfo, err)
358 }
359 }
360
@@ -365,7 +368,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error
368 err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2)
369 }
370 }
368 - return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err)
371 + return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err)
372 }
373 }
374
@@ -439,8 +442,10 @@ func (ctx Context) Storage() certmagic.Storage {
442 return ctx.cfg.storage
443 }
444
445 +// TODO: aw man, can I please change this?
446 // Logger returns a logger that can be used by mod.
447 func (ctx Context) Logger(mod Module) *zap.Logger {
448 + // TODO: if mod is nil, use ctx.Module() instead...
449 if ctx.cfg == nil {
450 // often the case in tests; just use a dev logger
451 l, err := zap.NewDevelopment()
@@ -451,3 +456,34 @@ func (ctx Context) Logger(mod Module) *zap.Logger {
456 }
457 return ctx.cfg.Logging.Logger(mod)
458 }
459 +
460 +// TODO: use this
461 +// // Logger returns a logger that can be used by the current module.
462 +// func (ctx Context) Log() *zap.Logger {
463 +// if ctx.cfg == nil {
464 +// // often the case in tests; just use a dev logger
465 +// l, err := zap.NewDevelopment()
466 +// if err != nil {
467 +// panic("config missing, unable to create dev logger: " + err.Error())
468 +// }
469 +// return l
470 +// }
471 +// return ctx.cfg.Logging.Logger(ctx.Module())
472 +// }
473 +
474 +// Modules returns the lineage of modules that this context provisioned,
475 +// with the most recent/current module being last in the list.
476 +func (ctx Context) Modules() []Module {
477 + mods := make([]Module, len(ctx.ancestry))
478 + copy(mods, ctx.ancestry)
479 + return mods
480 +}
481 +
482 +// Module returns the current module, or the most recent one
483 +// provisioned by the context.
484 +func (ctx Context) Module() Module {
485 + if len(ctx.ancestry) == 0 {
486 + return nil
487 + }
488 + return ctx.ancestry[len(ctx.ancestry)-1]
489 +}
go.mod
+1 -1
@@ -7,7 +7,7 @@ require (
7 github.com/Masterminds/sprig/v3 v3.2.2
8 github.com/alecthomas/chroma v0.10.0
9 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
10 - github.com/caddyserver/certmagic v0.16.3
10 + github.com/caddyserver/certmagic v0.17.0
11 github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
12 github.com/go-chi/chi v4.1.2+incompatible
13 github.com/google/cel-go v0.12.4
go.sum
+2 -2
@@ -131,8 +131,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
131 github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
132 github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
133 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
134 -github.com/caddyserver/certmagic v0.16.3 h1:1ZbiU7y5X0MnDjBTXywUbPMs/ScHbgCeeCy/LPh4IZk=
135 -github.com/caddyserver/certmagic v0.16.3/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM=
134 +github.com/caddyserver/certmagic v0.17.0 h1:AHHvvmv6SNcq0vK5BgCevQqYMV8GNprVk6FWZzx8d+Q=
135 +github.com/caddyserver/certmagic v0.17.0/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM=
136 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
137 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
138 github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
modules/caddyevents/app.go new
+367
@@ -0,0 +1,367 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package caddyevents
16 +
17 +import (
18 + "context"
19 + "encoding/json"
20 + "errors"
21 + "fmt"
22 + "strings"
23 + "time"
24 +
25 + "github.com/caddyserver/caddy/v2"
26 + "github.com/google/uuid"
27 + "go.uber.org/zap"
28 +)
29 +
30 +func init() {
31 + caddy.RegisterModule(App{})
32 +}
33 +
34 +// App implements a global eventing system within Caddy.
35 +// Modules can emit and subscribe to events, providing
36 +// hooks into deep parts of the code base that aren't
37 +// otherwise accessible. Events provide information about
38 +// what and when things are happening, and this facility
39 +// allows handlers to take action when events occur,
40 +// add information to the event's metadata, and even
41 +// control program flow in some cases.
42 +//
43 +// Events are propagated in a DOM-like fashion. An event
44 +// emitted from module `a.b.c` (the "origin") will first
45 +// invoke handlers listening to `a.b.c`, then `a.b`,
46 +// then `a`, then those listening regardless of origin.
47 +// If a handler returns the special error Aborted, then
48 +// propagation immediately stops and the event is marked
49 +// as aborted. Emitters may optionally choose to adjust
50 +// program flow based on an abort.
51 +//
52 +// Modules can subscribe to events by origin and/or name.
53 +// A handler is invoked only if it is subscribed to the
54 +// event by name and origin. Subscriptions should be
55 +// registered during the provisioning phase, before apps
56 +// are started.
57 +//
58 +// Event handlers are fired synchronously as part of the
59 +// regular flow of the program. This allows event handlers
60 +// to control the flow of the program if the origin permits
61 +// it and also allows handlers to convey new information
62 +// back into the origin module before it continues.
63 +// In essence, event handlers are similar to HTTP
64 +// middleware handlers.
65 +//
66 +// Event bindings/subscribers are unordered; i.e.
67 +// event handlers are invoked in an arbitrary order.
68 +// Event handlers should not rely on the logic of other
69 +// handlers to succeed.
70 +//
71 +// The entirety of this app module is EXPERIMENTAL and
72 +// subject to change. Pay attention to release notes.
73 +type App struct {
74 + // Subscriptions bind handlers to one or more events
75 + // either globally or scoped to specific modules or module
76 + // namespaces.
77 + Subscriptions []*Subscription `json:"subscriptions,omitempty"`
78 +
79 + // Map of event name to map of module ID/namespace to handlers
80 + subscriptions map[string]map[caddy.ModuleID][]Handler
81 +
82 + logger *zap.Logger
83 + started bool
84 +}
85 +
86 +// Subscription represents binding of one or more handlers to
87 +// one or more events.
88 +type Subscription struct {
89 + // The name(s) of the event(s) to bind to. Default: all events.
90 + Events []string `json:"events,omitempty"`
91 +
92 + // The ID or namespace of the module(s) from which events
93 + // originate to listen to for events. Default: all modules.
94 + //
95 + // Events propagate up, so events emitted by module "a.b.c"
96 + // will also trigger the event for "a.b" and "a". Thus, to
97 + // receive all events from "a.b.c" and "a.b.d", for example,
98 + // one can subscribe to either "a.b" or all of "a" entirely.
99 + Modules []caddy.ModuleID `json:"modules,omitempty"`
100 +
101 + // The event handler modules. These implement the actual
102 + // behavior to invoke when an event occurs. At least one
103 + // handler is required.
104 + HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=events.handlers inline_key=handler"`
105 +
106 + // The decoded handlers; Go code that is subscribing to
107 + // an event should set this field directly; HandlersRaw
108 + // is meant for JSON configuration to fill out this field.
109 + Handlers []Handler `json:"-"`
110 +}
111 +
112 +// CaddyModule returns the Caddy module information.
113 +func (App) CaddyModule() caddy.ModuleInfo {
114 + return caddy.ModuleInfo{
115 + ID: "events",
116 + New: func() caddy.Module { return new(App) },
117 + }
118 +}
119 +
120 +// Provision sets up the app.
121 +func (app *App) Provision(ctx caddy.Context) error {
122 + app.logger = ctx.Logger(app)
123 + app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler)
124 +
125 + for _, sub := range app.Subscriptions {
126 + if sub.HandlersRaw != nil {
127 + handlersIface, err := ctx.LoadModule(sub, "HandlersRaw")
128 + if err != nil {
129 + return fmt.Errorf("loading event subscriber modules: %v", err)
130 + }
131 + for _, h := range handlersIface.([]any) {
132 + sub.Handlers = append(sub.Handlers, h.(Handler))
133 + }
134 + if len(sub.Handlers) == 0 {
135 + // pointless to bind without any handlers
136 + return fmt.Errorf("no handlers defined")
137 + }
138 + }
139 + }
140 +
141 + return nil
142 +}
143 +
144 +// Start runs the app.
145 +func (app *App) Start() error {
146 + for _, sub := range app.Subscriptions {
147 + if err := app.Subscribe(sub); err != nil {
148 + return err
149 + }
150 + }
151 +
152 + app.started = true
153 +
154 + return nil
155 +}
156 +
157 +// Stop gracefully shuts down the app.
158 +func (app *App) Stop() error {
159 + return nil
160 +}
161 +
162 +// Subscribe binds one or more event handlers to one or more events
163 +// according to the subscription s. For now, subscriptions can only
164 +// be created during the provision phase; new bindings cannot be
165 +// created after the events app has started.
166 +func (app *App) Subscribe(s *Subscription) error {
167 + if app.started {
168 + return fmt.Errorf("events already started; new subscriptions closed")
169 + }
170 +
171 + // handle special case of catch-alls (omission of event name or module space implies all)
172 + if len(s.Events) == 0 {
173 + s.Events = []string{""}
174 + }
175 + if len(s.Modules) == 0 {
176 + s.Modules = []caddy.ModuleID{""}
177 + }
178 +
179 + for _, eventName := range s.Events {
180 + if app.subscriptions[eventName] == nil {
181 + app.subscriptions[eventName] = make(map[caddy.ModuleID][]Handler)
182 + }
183 + for _, originModule := range s.Modules {
184 + app.subscriptions[eventName][originModule] = append(app.subscriptions[eventName][originModule], s.Handlers...)
185 + }
186 + }
187 +
188 + return nil
189 +}
190 +
191 +// On is syntactic sugar for Subscribe() that binds a single handler
192 +// to a single event from any module. If the eventName is empty string,
193 +// it counts for all events.
194 +func (app *App) On(eventName string, handler Handler) error {
195 + return app.Subscribe(&Subscription{
196 + Events: []string{eventName},
197 + Handlers: []Handler{handler},
198 + })
199 +}
200 +
201 +// Emit creates and dispatches an event named eventName to all relevant handlers with
202 +// the metadata data. Events are emitted and propagated synchronously. The returned Event
203 +// value will have any additional information from the invoked handlers.
204 +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event {
205 + id, err := uuid.NewRandom()
206 + if err != nil {
207 + app.logger.Error("failed generating new event ID",
208 + zap.Error(err),
209 + zap.String("event", eventName))
210 + }
211 +
212 + eventName = strings.ToLower(eventName)
213 +
214 + e := Event{
215 + id: id,
216 + ts: time.Now(),
217 + name: eventName,
218 + origin: ctx.Module(),
219 + data: data,
220 + }
221 +
222 + // add event info to replacer, make sure it's in the context
223 + repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
224 + if !ok {
225 + repl = caddy.NewReplacer()
226 + ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl)
227 + }
228 + repl.Map(func(key string) (any, bool) {
229 + switch key {
230 + case "event":
231 + return e, true
232 + case "event.id":
233 + return e.id, true
234 + case "event.name":
235 + return e.name, true
236 + case "event.time":
237 + return e.ts, true
238 + case "event.time_unix":
239 + return e.ts.UnixMilli(), true
240 + case "event.module":
241 + return e.origin.CaddyModule().ID, true
242 + case "event.data":
243 + return e.data, true
244 + }
245 +
246 + if strings.HasPrefix(key, "event.data.") {
247 + key = strings.TrimPrefix(key, "event.data.")
248 + if val, ok := data[key]; ok {
249 + return val, true
250 + }
251 + }
252 +
253 + return nil, false
254 + })
255 +
256 + app.logger.Debug("event",
257 + zap.String("name", e.name),
258 + zap.String("id", e.id.String()),
259 + zap.String("origin", e.origin.CaddyModule().String()),
260 + zap.Any("data", e.data),
261 + )
262 +
263 + // invoke handlers bound to the event by name and also all events; this for loop
264 + // iterates twice at most: once for the event name, once for "" (all events)
265 + for {
266 + moduleID := e.origin.CaddyModule().ID
267 +
268 + // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "")
269 + for {
270 + if app.subscriptions[eventName] == nil {
271 + break // shortcut if event not bound at all
272 + }
273 +
274 + for _, handler := range app.subscriptions[eventName][moduleID] {
275 + if err := handler.Handle(ctx, e); err != nil {
276 + aborted := errors.Is(err, ErrAborted)
277 +
278 + app.logger.Error("handler error",
279 + zap.Error(err),
280 + zap.Bool("aborted", aborted))
281 +
282 + if aborted {
283 + e.Aborted = err
284 + return e
285 + }
286 + }
287 + }
288 +
289 + if moduleID == "" {
290 + break
291 + }
292 + lastDot := strings.LastIndex(string(moduleID), ".")
293 + if lastDot < 0 {
294 + moduleID = "" // include handlers bound to events regardless of module
295 + } else {
296 + moduleID = moduleID[:lastDot]
297 + }
298 + }
299 +
300 + // include handlers listening to all events
301 + if eventName == "" {
302 + break
303 + }
304 + eventName = ""
305 + }
306 +
307 + return e
308 +}
309 +
310 +// Event represents something that has happened or is happening.
311 +type Event struct {
312 + id uuid.UUID
313 + ts time.Time
314 + name string
315 + origin caddy.Module
316 + data map[string]any
317 +
318 + // If non-nil, the event has been aborted, meaning
319 + // propagation has stopped to other handlers and
320 + // the code should stop what it was doing. Emitters
321 + // may choose to use this as a signal to adjust their
322 + // code path appropriately.
323 + Aborted error
324 +}
325 +
326 +// CloudEvent exports event e as a structure that, when
327 +// serialized as JSON, is compatible with the
328 +// CloudEvents spec.
329 +func (e Event) CloudEvent() CloudEvent {
330 + dataJSON, _ := json.Marshal(e.data)
331 + return CloudEvent{
332 + ID: e.id.String(),
333 + Source: e.origin.CaddyModule().String(),
334 + SpecVersion: "1.0",
335 + Type: e.name,
336 + Time: e.ts,
337 + DataContentType: "application/json",
338 + Data: dataJSON,
339 + }
340 +}
341 +
342 +// CloudEvent is a JSON-serializable structure that
343 +// is compatible with the CloudEvents specification.
344 +// See https://cloudevents.io.
345 +type CloudEvent struct {
346 + ID string `json:"id"`
347 + Source string `json:"source"`
348 + SpecVersion string `json:"specversion"`
349 + Type string `json:"type"`
350 + Time time.Time `json:"time"`
351 + DataContentType string `json:"datacontenttype,omitempty"`
352 + Data json.RawMessage `json:"data,omitempty"`
353 +}
354 +
355 +// ErrAborted cancels an event.
356 +var ErrAborted = errors.New("event aborted")
357 +
358 +// Handler is a type that can handle events.
359 +type Handler interface {
360 + Handle(context.Context, Event) error
361 +}
362 +
363 +// Interface guards
364 +var (
365 + _ caddy.App = (*App)(nil)
366 + _ caddy.Provisioner = (*App)(nil)
367 +)
modules/caddyevents/eventsconfig/caddyfile.go new
+88
@@ -0,0 +1,88 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +// Package eventsconfig is for configuring caddyevents.App with the
16 +// Caddyfile. This code can't be in the caddyevents package because
17 +// the httpcaddyfile package imports caddyhttp, which imports
18 +// caddyevents: hence, it creates an import cycle.
19 +package eventsconfig
20 +
21 +import (
22 + "encoding/json"
23 +
24 + "github.com/caddyserver/caddy/v2/caddyconfig"
25 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
26 + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
27 + "github.com/caddyserver/caddy/v2/modules/caddyevents"
28 +)
29 +
30 +func init() {
31 + httpcaddyfile.RegisterGlobalOption("events", parseApp)
32 +}
33 +
34 +// parseApp configures the "events" global option from Caddyfile to set up the events app.
35 +// Syntax:
36 +//
37 +// events {
38 +// on <event> <handler_module...>
39 +// }
40 +//
41 +// If <event> is *, then it will bind to all events.
42 +func parseApp(d *caddyfile.Dispenser, _ any) (any, error) {
43 + app := new(caddyevents.App)
44 +
45 + // consume the option name
46 + if !d.Next() {
47 + return nil, d.ArgErr()
48 + }
49 +
50 + // handle the block
51 + for d.NextBlock(0) {
52 + switch d.Val() {
53 + case "on":
54 + if !d.NextArg() {
55 + return nil, d.ArgErr()
56 + }
57 + eventName := d.Val()
58 + if eventName == "*" {
59 + eventName = ""
60 + }
61 +
62 + if !d.NextArg() {
63 + return nil, d.ArgErr()
64 + }
65 + handlerName := d.Val()
66 + modID := "events.handlers." + handlerName
67 + unm, err := caddyfile.UnmarshalModule(d, modID)
68 + if err != nil {
69 + return nil, err
70 + }
71 +
72 + app.Subscriptions = append(app.Subscriptions, &caddyevents.Subscription{
73 + Events: []string{eventName},
74 + HandlersRaw: []json.RawMessage{
75 + caddyconfig.JSONModuleObject(unm, "handler", handlerName, nil),
76 + },
77 + })
78 +
79 + default:
80 + return nil, d.ArgErr()
81 + }
82 + }
83 +
84 + return httpcaddyfile.App{
85 + Name: "events",
86 + Value: caddyconfig.JSON(app, nil),
87 + }, nil
88 +}
modules/caddyhttp/app.go
+8
@@ -24,6 +24,7 @@ import (
24 "time"
25
26 "github.com/caddyserver/caddy/v2"
27 + "github.com/caddyserver/caddy/v2/modules/caddyevents"
28 "github.com/caddyserver/caddy/v2/modules/caddytls"
29 "go.uber.org/zap"
30 "golang.org/x/net/http2"
@@ -161,6 +162,11 @@ func (app *App) Provision(ctx caddy.Context) error {
162 app.ctx = ctx
163 app.logger = ctx.Logger(app)
164
165 + eventsAppIface, err := ctx.App("events")
166 + if err != nil {
167 + return fmt.Errorf("getting events app: %v", err)
168 + }
169 +
170 repl := caddy.NewReplacer()
171
172 // this provisions the matchers for each route,
@@ -175,6 +181,8 @@ func (app *App) Provision(ctx caddy.Context) error {
181 for srvName, srv := range app.Servers {
182 srv.name = srvName
183 srv.tlsApp = app.tlsApp
184 + srv.events = eventsAppIface.(*caddyevents.App)
185 + srv.ctx = ctx
186 srv.logger = app.logger.Named("log")
187 srv.errorLogger = app.logger.Named("log.error")
188 srv.shutdownAtMu = new(sync.RWMutex)
modules/caddyhttp/reverseproxy/healthchecks.go
+13 -5
@@ -284,6 +284,13 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
284 }
285 }
286
287 + markUnhealthy := func() {
288 + // dispatch an event that the host newly became unhealthy
289 + if upstream.setHealthy(false) {
290 + h.events.Emit(h.ctx, "unhealthy", map[string]any{"host": hostAddr})
291 + }
292 + }
293 +
294 // do the request, being careful to tame the response body
295 resp, err := h.HealthChecks.Active.httpClient.Do(req)
296 if err != nil {
@@ -291,7 +298,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
298 zap.String("host", hostAddr),
299 zap.Error(err),
300 )
294 - upstream.setHealthy(false)
301 + markUnhealthy()
302 return nil
303 }
304 var body io.Reader = resp.Body
@@ -311,7 +318,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
318 zap.Int("status_code", resp.StatusCode),
319 zap.String("host", hostAddr),
320 )
314 - upstream.setHealthy(false)
321 + markUnhealthy()
322 return nil
323 }
324 } else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
@@ -319,7 +326,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
326 zap.Int("status_code", resp.StatusCode),
327 zap.String("host", hostAddr),
328 )
322 - upstream.setHealthy(false)
329 + markUnhealthy()
330 return nil
331 }
332
@@ -331,14 +338,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
338 zap.String("host", hostAddr),
339 zap.Error(err),
340 )
334 - upstream.setHealthy(false)
341 + markUnhealthy()
342 return nil
343 }
344 if !h.HealthChecks.Active.bodyRegexp.Match(bodyBytes) {
345 h.HealthChecks.Active.logger.Info("response body failed expectations",
346 zap.String("host", hostAddr),
347 )
341 - upstream.setHealthy(false)
348 + markUnhealthy()
349 return nil
350 }
351 }
@@ -346,6 +353,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre
353 // passed health check parameters, so mark as healthy
354 if upstream.setHealthy(true) {
355 h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr))
356 + h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr})
357 }
358
359 return nil
modules/caddyhttp/reverseproxy/reverseproxy.go
+7
@@ -36,6 +36,7 @@ import (
36
37 "github.com/caddyserver/caddy/v2"
38 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
39 + "github.com/caddyserver/caddy/v2/modules/caddyevents"
40 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
41 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
42 "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
@@ -193,6 +194,7 @@ type Handler struct {
194
195 ctx caddy.Context
196 logger *zap.Logger
197 + events *caddyevents.App
198 }
199
200 // CaddyModule returns the Caddy module information.
@@ -205,6 +207,11 @@ func (Handler) CaddyModule() caddy.ModuleInfo {
207
208 // Provision ensures that h is set up properly before use.
209 func (h *Handler) Provision(ctx caddy.Context) error {
210 + eventAppIface, err := ctx.App("events")
211 + if err != nil {
212 + return fmt.Errorf("getting events app: %v", err)
213 + }
214 + h.events = eventAppIface.(*caddyevents.App)
215 h.ctx = ctx
216 h.logger = ctx.Logger(h)
217
modules/caddyhttp/server.go
+3
@@ -30,6 +30,7 @@ import (
30 "time"
31
32 "github.com/caddyserver/caddy/v2"
33 + "github.com/caddyserver/caddy/v2/modules/caddyevents"
34 "github.com/caddyserver/caddy/v2/modules/caddytls"
35 "github.com/caddyserver/certmagic"
36 "github.com/lucas-clemente/quic-go/http3"
@@ -154,9 +155,11 @@ type Server struct {
155 listeners []net.Listener
156
157 tlsApp *caddytls.TLS
158 + events *caddyevents.App
159 logger *zap.Logger
160 accessLogger *zap.Logger
161 errorLogger *zap.Logger
162 + ctx caddy.Context
163
164 server *http.Server
165 h3server *http3.Server
modules/caddytls/automation.go
+1
@@ -256,6 +256,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
256 MustStaple: ap.MustStaple,
257 RenewalWindowRatio: ap.RenewalWindowRatio,
258 KeySource: keySource,
259 + OnEvent: tlsApp.onEvent,
260 OnDemand: ond,
261 OCSP: certmagic.OCSPConfig{
262 DisableStapling: ap.DisableOCSPStapling,
modules/caddytls/tls.go
+15
@@ -15,6 +15,7 @@
15 package caddytls
16
17 import (
18 + "context"
19 "crypto/tls"
20 "encoding/json"
21 "fmt"
@@ -25,6 +26,7 @@ import (
26 "time"
27
28 "github.com/caddyserver/caddy/v2"
29 + "github.com/caddyserver/caddy/v2/modules/caddyevents"
30 "github.com/caddyserver/certmagic"
31 "go.uber.org/zap"
32 )
@@ -73,6 +75,7 @@ type TLS struct {
75 storageCleanTicker *time.Ticker
76 storageCleanStop chan struct{}
77 logger *zap.Logger
78 + events *caddyevents.App
79 }
80
81 // CaddyModule returns the Caddy module information.
@@ -85,6 +88,11 @@ func (TLS) CaddyModule() caddy.ModuleInfo {
88
89 // Provision sets up the configuration for the TLS app.
90 func (t *TLS) Provision(ctx caddy.Context) error {
91 + eventsAppIface, err := ctx.App("events")
92 + if err != nil {
93 + return fmt.Errorf("getting events app: %v", err)
94 + }
95 + t.events = eventsAppIface.(*caddyevents.App)
96 t.ctx = ctx
97 t.logger = ctx.Logger(t)
98 repl := caddy.NewReplacer()
@@ -189,6 +197,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
197 magic := certmagic.New(t.certCache, certmagic.Config{
198 Storage: ctx.Storage(),
199 Logger: t.logger,
200 + OnEvent: t.onEvent,
201 OCSP: certmagic.OCSPConfig{
202 DisableStapling: t.DisableOCSPStapling,
203 },
@@ -514,6 +523,12 @@ func (t *TLS) storageCleanInterval() time.Duration {
523 return defaultStorageCleanInterval
524 }
525
526 +// onEvent translates CertMagic events into Caddy events then dispatches them.
527 +func (t *TLS) onEvent(ctx context.Context, eventName string, data map[string]any) error {
528 + evt := t.events.Emit(t.ctx, eventName, data)
529 + return evt.Aborted
530 +}
531 +
532 // CertificateLoader is a type that can load certificates.
533 // Certificates can optionally be associated with tags.
534 type CertificateLoader interface {
modules/standard/imports.go
+2
@@ -3,6 +3,8 @@ package standard
3 import (
4 // standard Caddy modules
5 _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
6 + _ "github.com/caddyserver/caddy/v2/modules/caddyevents"
7 + _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig"
8 _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard"
9 _ "github.com/caddyserver/caddy/v2/modules/caddypki"
10 _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver"
usagepool.go
+10 -10
@@ -25,15 +25,15 @@ import (
25 // only inserted if they do not already exist. There
26 // are two ways to add values to the pool:
27 //
28 -// 1) LoadOrStore will increment usage and store the
29 -// value immediately if it does not already exist.
30 -// 2) LoadOrNew will atomically check for existence
31 -// and construct the value immediately if it does
32 -// not already exist, or increment the usage
33 -// otherwise, then store that value in the pool.
34 -// When the constructed value is finally deleted
35 -// from the pool (when its usage reaches 0), it
36 -// will be cleaned up by calling Destruct().
28 +// 1. LoadOrStore will increment usage and store the
29 +// value immediately if it does not already exist.
30 +// 2. LoadOrNew will atomically check for existence
31 +// and construct the value immediately if it does
32 +// not already exist, or increment the usage
33 +// otherwise, then store that value in the pool.
34 +// When the constructed value is finally deleted
35 +// from the pool (when its usage reaches 0), it
36 +// will be cleaned up by calling Destruct().
37 //
38 // The use of LoadOrNew allows values to be created
39 // and reused and finally cleaned up only once, even
@@ -196,7 +196,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) {
196
197 // References returns the number of references (count of usages) to a
198 // key in the pool, and true if the key exists, or false otherwise.
199 -func (up *UsagePool) References(key interface{}) (int, bool) {
199 +func (up *UsagePool) References(key any) (int, bool) {
200 up.RLock()
201 upv, loaded := up.pool[key]
202 up.RUnlock()