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