From 084b436cc53715c0bc3e25a8ca033b41fd34d6d5 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 30 Apr 2021 01:59:41 -0400 Subject: [PATCH 01/14] event: Implement event system Rename app --- modules/caddyevent/app.go | 62 ++++++++++++++++ modules/caddyevent/event.go | 140 ++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 modules/caddyevent/app.go create mode 100644 modules/caddyevent/event.go diff --git a/modules/caddyevent/app.go b/modules/caddyevent/app.go new file mode 100644 index 00000000000..0e64ae7d8f6 --- /dev/null +++ b/modules/caddyevent/app.go @@ -0,0 +1,62 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevent + +import ( + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(EventApp{}) +} + +// EventApp is a global event system. +type EventApp struct { +} + +// CaddyModule returns the Caddy module information. +func (EventApp) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "event", + New: func() caddy.Module { return new(EventApp) }, + } +} + +// Provision sets up the app. +func (app *EventApp) Provision(ctx caddy.Context) error { + return nil +} + +// Validate ensures the app's configuration is valid. +func (app *EventApp) Validate() error { + return nil +} + +// Start runs the app. +func (app *EventApp) Start() error { + return nil +} + +// Stop gracefully shuts down the app. +func (app *EventApp) Stop() error { + return nil +} + +// Interface guards +var ( + _ caddy.App = (*EventApp)(nil) + _ caddy.Provisioner = (*EventApp)(nil) + _ caddy.Validator = (*EventApp)(nil) +) diff --git a/modules/caddyevent/event.go b/modules/caddyevent/event.go new file mode 100644 index 00000000000..5b9388705ea --- /dev/null +++ b/modules/caddyevent/event.go @@ -0,0 +1,140 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevent + +import ( + "github.com/caddyserver/caddy/v2" +) + +// DataMap is a key-value pair map. +type DataMap map[string]interface{} + +// Event interface +type Event interface { + // Get the module ID of the event. + ID() caddy.ModuleID + + // Get an item from the data map by key. + Get(key string) interface{} + + // Set an item in the data map by key. + Set(key string, val interface{}) + + // Get the entire data map. + Data() DataMap + + // Replace the entire data map. + SetData(DataMap) + + // StopPropagation sets the event to no longer propagate + // to subsequent event listeners. + StopPropagation(bool) + + // IsPropagationStopped returns whether the event is set + // to no longer propagate. In other words, this is + // whether the event dispatcher should stop handling + // this event and skip passing it to any subsequent + // event listeners. + IsPropagationStopped() bool +} + +// GenericEvent a generic event, which can be used +// with composition to provide baseline functionality +// for custom events. +type GenericEvent struct { + // The module ID of the event. + id caddy.ModuleID + + // Key-value data pairs of contextual information. + data DataMap + + // Whether the event handling should be aborted, + // and no event handlers should be called after + // this point. Allows for "middleware chain" type + // of functionality. + propagationStopped bool +} + +// NewGeneric creates a generic event instance. +func NewGeneric(id caddy.ModuleID, data DataMap) *GenericEvent { + if data == nil { + data = make(DataMap) + } + + return &GenericEvent{ + id: id, + data: data, + } +} + +// SetID sets the module ID of the event. +func (e *GenericEvent) SetID(id caddy.ModuleID) { + e.id = id +} + +// Get the module ID of the event. +func (e *GenericEvent) ID() caddy.ModuleID { + return e.id +} + +// Get an item from the data map by key. +func (e *GenericEvent) Get(key string) interface{} { + if v, ok := e.data[key]; ok { + return v + } + + return nil +} + +// Set an item in the data map by key. +func (e *GenericEvent) Set(key string, val interface{}) { + if e.data == nil { + e.data = make(DataMap) + } + + e.data[key] = val +} + +// Data returns the entire data map. +func (e *GenericEvent) Data() DataMap { + return e.data +} + +// SetData overwrites the entire data map. +func (e *GenericEvent) SetData(data DataMap) { + if data != nil { + e.data = data + } +} + +// StopPropagation sets the event to no longer propagate +// to subsequent event listeners. +func (e *GenericEvent) StopPropagation(abort bool) { + e.propagationStopped = abort +} + +// IsPropagationStopped returns whether the event is set +// to no longer propagate. In other words, this is +// whether the event dispatcher should stop handling +// this event and skip passing it to any subsequent +// event listeners. +func (e *GenericEvent) IsPropagationStopped() bool { + return e.propagationStopped +} + +// Interface guards +var ( + _ Event = (*GenericEvent)(nil) +) From 574290a9c182ba3dc2d2922b4ad93eda86b33945 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 30 Apr 2021 02:58:39 -0400 Subject: [PATCH 02/14] event: Implement listener/subscriber Move sub interface to listener --- modules/caddyevent/listener.go | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 modules/caddyevent/listener.go diff --git a/modules/caddyevent/listener.go b/modules/caddyevent/listener.go new file mode 100644 index 00000000000..637aad07245 --- /dev/null +++ b/modules/caddyevent/listener.go @@ -0,0 +1,46 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevent + +import ( + "github.com/caddyserver/caddy/v2" +) + +// ListenerFunc is a function that can handle a dispatched event. +type ListenerFunc func(e Event) error + +// Handle runs the event listener on the given event. +func (fn ListenerFunc) Handle(e Event) error { + return fn(e) +} + +// Priority is a factor by which event listeners can be sorted. +type Priority int + +// ListenerEntry is a wrapper to allow associating a listener function +// to a priority factor when registering event subscribers and listeners. +type ListenerEntry struct { + Listener ListenerFunc + Priority Priority +} + +// Subscriber defines an interface for modules that wish +// to subscribe to dispatched events. +type Subscriber interface { + // SubscribedEvents returns a map of event IDs that + // this subscriber can handle, to the function that + // handles the event. + SubscribedEvents() map[caddy.ModuleID]ListenerEntry +} From e031c5bc7e64a0c8aacc16474b20dae18869c0d8 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Fri, 11 Jun 2021 22:34:56 -0400 Subject: [PATCH 03/14] event: Implement registering listeners, dispatching Better comment --- modules/caddyevent/app.go | 246 ++++++++++++++++++++++++++++---------- 1 file changed, 184 insertions(+), 62 deletions(-) diff --git a/modules/caddyevent/app.go b/modules/caddyevent/app.go index 0e64ae7d8f6..992526fd4fc 100644 --- a/modules/caddyevent/app.go +++ b/modules/caddyevent/app.go @@ -1,62 +1,184 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -func init() { - caddy.RegisterModule(EventApp{}) -} - -// EventApp is a global event system. -type EventApp struct { -} - -// CaddyModule returns the Caddy module information. -func (EventApp) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "event", - New: func() caddy.Module { return new(EventApp) }, - } -} - -// Provision sets up the app. -func (app *EventApp) Provision(ctx caddy.Context) error { - return nil -} - -// Validate ensures the app's configuration is valid. -func (app *EventApp) Validate() error { - return nil -} - -// Start runs the app. -func (app *EventApp) Start() error { - return nil -} - -// Stop gracefully shuts down the app. -func (app *EventApp) Stop() error { - return nil -} - -// Interface guards -var ( - _ caddy.App = (*EventApp)(nil) - _ caddy.Provisioner = (*EventApp)(nil) - _ caddy.Validator = (*EventApp)(nil) -) +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevent + +import ( + "fmt" + "sort" + + "github.com/caddyserver/caddy/v2" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(EventApp{}) +} + +// EventApp is a global event system. +type EventApp struct { + listeners map[caddy.ModuleID]map[Priority][]ListenerFunc + optimized map[caddy.ModuleID][]ListenerFunc + ready bool + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (EventApp) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "event", + New: func() caddy.Module { return new(EventApp) }, + } +} + +// Provision sets up the app. +func (app *EventApp) Provision(ctx caddy.Context) error { + app.listeners = make(map[caddy.ModuleID]map[Priority][]ListenerFunc) + app.logger = ctx.Logger(app) + + return nil +} + +// Validate ensures the app's configuration is valid. +func (app *EventApp) Validate() error { + return nil +} + +// Start runs the app. +func (app *EventApp) Start() error { + // optimize the event listeners to order them by priority. + app.optimizeListeners() + + // stop new listeners from being added, + // and allow events to be dispatched. + app.ready = true + + return nil +} + +// Stop gracefully shuts down the app. +func (app *EventApp) Stop() error { + return nil +} + +// RegisterSubscriber registers all the listeners from a subscriber. +// Modules may register themselves as subscribers during their Provision +// phase. Subscribers cannot be registered after the config is running. +func (app *EventApp) RegisterSubscriber(subscriber Subscriber) { + for eventID, entry := range subscriber.SubscribedEvents() { + app.RegisterListener(eventID, entry) + } +} + +// RegisterListener registers a single event listener. +// Modules may register listeners during their Provision phase. +// Listeners cannot be registered after the config is running. +func (app *EventApp) RegisterListener(eventID caddy.ModuleID, entry ListenerEntry) { + // if the app is already running, we don't allow adding new listeners. + if app.ready { + return + } + + if app.listeners[eventID] == nil { + app.listeners[eventID] = make(map[Priority][]ListenerFunc) + } + + // There may be more than one listener with the same priority, + // for the same event, so we have an array of listeners for + // each priority level. Listeners at the same priority level + // will be in the order they are registered, which will not + // have a guaranteed order because Caddy modules may be loaded + // in an arbitrary order. + app.listeners[eventID][entry.Priority] = append( + app.listeners[eventID][entry.Priority], + entry.Listener, + ) + + app.logger.Debug("registered listener", + zap.String("event", eventID.Name()), + zap.Int("priority", int(entry.Priority)), + ) +} + +// Dispatch passes the event through the configured listeners synchronously. +func (app *EventApp) Dispatch(event Event) error { + // if the app is not running, we don't allow dispatching events. + if !app.ready { + return fmt.Errorf("Cannot dispatch events until after the app is running") + } + + // find the listeners for this event + listeners, ok := app.optimized[event.ID()] + if !ok { + return nil + } + + app.logger.Debug("dispatching", zap.String("event", event.ID().Name())) + + for _, listener := range listeners { + // listeners may mark the event to stop subsequent + // listeners from running on this event. + if event.IsPropagationStopped() { + app.logger.Debug("propogation stopped", zap.String("event", event.ID().Name())) + break + } + + // run the listener. + err := listener(event) + if err != nil { + return err + } + } + + return nil +} + +// AsyncDispatch passes the event through the configured listeners asynchronously. +func (app *EventApp) AsyncDispatch(event Event) { + go func(event Event) { + _ = app.Dispatch(event) + }(event) +} + +// optimizeListeners orders the listeners by priority. +func (app *EventApp) optimizeListeners() { + app.optimized = make(map[caddy.ModuleID][]ListenerFunc) + for eventID, priorities := range app.listeners { + app.optimized[eventID] = []ListenerFunc{} + + // sort the priorities (highest priority value first) + keys := make([]int, 0) + for k := range priorities { + keys = append(keys, int(k)) + } + sort.Sort(sort.Reverse(sort.IntSlice(keys))) + + // add all the listeners in order of priority + for _, k := range keys { + for _, listener := range priorities[Priority(k)] { + app.optimized[eventID] = append(app.optimized[eventID], listener) + } + } + } + + // we no longer need this map once we're running + app.listeners = nil +} + +// Interface guards +var ( + _ caddy.App = (*EventApp)(nil) + _ caddy.Provisioner = (*EventApp)(nil) + _ caddy.Validator = (*EventApp)(nil) +) From 6c499f6f62ef82ef20216023b1cc95935f67937d Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Thu, 31 Mar 2022 01:57:24 -0400 Subject: [PATCH 04/14] event: Line endings, std module imports, sub modules via JSON config fix --- modules/caddyevent/app.go | 20 +++ modules/caddyevent/event.go | 280 ++++++++++++++++----------------- modules/caddyevent/listener.go | 92 +++++------ modules/standard/imports.go | 1 + 4 files changed, 207 insertions(+), 186 deletions(-) diff --git a/modules/caddyevent/app.go b/modules/caddyevent/app.go index 992526fd4fc..5411ef99673 100644 --- a/modules/caddyevent/app.go +++ b/modules/caddyevent/app.go @@ -15,6 +15,7 @@ package caddyevent import ( + "encoding/json" "fmt" "sort" @@ -28,6 +29,9 @@ func init() { // EventApp is a global event system. type EventApp struct { + // Registers each of these event subscribers + SubscribersRaw []json.RawMessage `json:"subscribers,omitempty" caddy:"namespace=event.subscribers inline_key=subscriber"` + listeners map[caddy.ModuleID]map[Priority][]ListenerFunc optimized map[caddy.ModuleID][]ListenerFunc ready bool @@ -47,6 +51,17 @@ func (app *EventApp) Provision(ctx caddy.Context) error { app.listeners = make(map[caddy.ModuleID]map[Priority][]ListenerFunc) app.logger = ctx.Logger(app) + // register all the configured subscribers + if app.SubscribersRaw != nil { + subscribersIface, err := ctx.LoadModule(app, "SubscribersRaw") + if err != nil { + return fmt.Errorf("loading event subscriber modules: %v", err) + } + for _, subscriber := range subscribersIface.([]Subscriber) { + app.RegisterSubscriber(subscriber) + } + } + return nil } @@ -87,6 +102,7 @@ func (app *EventApp) RegisterSubscriber(subscriber Subscriber) { func (app *EventApp) RegisterListener(eventID caddy.ModuleID, entry ListenerEntry) { // if the app is already running, we don't allow adding new listeners. if app.ready { + // TODO: Panic or something? return } @@ -137,6 +153,10 @@ func (app *EventApp) Dispatch(event Event) error { // run the listener. err := listener(event) if err != nil { + app.logger.Error("listener error", + zap.String("event", event.ID().Name()), + zap.Error(err), + ) return err } } diff --git a/modules/caddyevent/event.go b/modules/caddyevent/event.go index 5b9388705ea..6255e5e6b66 100644 --- a/modules/caddyevent/event.go +++ b/modules/caddyevent/event.go @@ -1,140 +1,140 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -// DataMap is a key-value pair map. -type DataMap map[string]interface{} - -// Event interface -type Event interface { - // Get the module ID of the event. - ID() caddy.ModuleID - - // Get an item from the data map by key. - Get(key string) interface{} - - // Set an item in the data map by key. - Set(key string, val interface{}) - - // Get the entire data map. - Data() DataMap - - // Replace the entire data map. - SetData(DataMap) - - // StopPropagation sets the event to no longer propagate - // to subsequent event listeners. - StopPropagation(bool) - - // IsPropagationStopped returns whether the event is set - // to no longer propagate. In other words, this is - // whether the event dispatcher should stop handling - // this event and skip passing it to any subsequent - // event listeners. - IsPropagationStopped() bool -} - -// GenericEvent a generic event, which can be used -// with composition to provide baseline functionality -// for custom events. -type GenericEvent struct { - // The module ID of the event. - id caddy.ModuleID - - // Key-value data pairs of contextual information. - data DataMap - - // Whether the event handling should be aborted, - // and no event handlers should be called after - // this point. Allows for "middleware chain" type - // of functionality. - propagationStopped bool -} - -// NewGeneric creates a generic event instance. -func NewGeneric(id caddy.ModuleID, data DataMap) *GenericEvent { - if data == nil { - data = make(DataMap) - } - - return &GenericEvent{ - id: id, - data: data, - } -} - -// SetID sets the module ID of the event. -func (e *GenericEvent) SetID(id caddy.ModuleID) { - e.id = id -} - -// Get the module ID of the event. -func (e *GenericEvent) ID() caddy.ModuleID { - return e.id -} - -// Get an item from the data map by key. -func (e *GenericEvent) Get(key string) interface{} { - if v, ok := e.data[key]; ok { - return v - } - - return nil -} - -// Set an item in the data map by key. -func (e *GenericEvent) Set(key string, val interface{}) { - if e.data == nil { - e.data = make(DataMap) - } - - e.data[key] = val -} - -// Data returns the entire data map. -func (e *GenericEvent) Data() DataMap { - return e.data -} - -// SetData overwrites the entire data map. -func (e *GenericEvent) SetData(data DataMap) { - if data != nil { - e.data = data - } -} - -// StopPropagation sets the event to no longer propagate -// to subsequent event listeners. -func (e *GenericEvent) StopPropagation(abort bool) { - e.propagationStopped = abort -} - -// IsPropagationStopped returns whether the event is set -// to no longer propagate. In other words, this is -// whether the event dispatcher should stop handling -// this event and skip passing it to any subsequent -// event listeners. -func (e *GenericEvent) IsPropagationStopped() bool { - return e.propagationStopped -} - -// Interface guards -var ( - _ Event = (*GenericEvent)(nil) -) +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevent + +import ( + "github.com/caddyserver/caddy/v2" +) + +// DataMap is a key-value pair map. +type DataMap map[string]interface{} + +// Event interface +type Event interface { + // Get the module ID of the event. + ID() caddy.ModuleID + + // Get an item from the data map by key. + Get(key string) interface{} + + // Set an item in the data map by key. + Set(key string, val interface{}) + + // Get the entire data map. + Data() DataMap + + // Replace the entire data map. + SetData(DataMap) + + // StopPropagation sets the event to no longer propagate + // to subsequent event listeners. + StopPropagation(bool) + + // IsPropagationStopped returns whether the event is set + // to no longer propagate. In other words, this is + // whether the event dispatcher should stop handling + // this event and skip passing it to any subsequent + // event listeners. + IsPropagationStopped() bool +} + +// GenericEvent a generic event, which can be used +// with composition to provide baseline functionality +// for custom events. +type GenericEvent struct { + // The module ID of the event. + id caddy.ModuleID + + // Key-value data pairs of contextual information. + data DataMap + + // Whether the event handling should be aborted, + // and no event handlers should be called after + // this point. Allows for "middleware chain" type + // of functionality. + propagationStopped bool +} + +// NewGeneric creates a generic event instance. +func NewGeneric(id caddy.ModuleID, data DataMap) *GenericEvent { + if data == nil { + data = make(DataMap) + } + + return &GenericEvent{ + id: id, + data: data, + } +} + +// SetID sets the module ID of the event. +func (e *GenericEvent) SetID(id caddy.ModuleID) { + e.id = id +} + +// Get the module ID of the event. +func (e *GenericEvent) ID() caddy.ModuleID { + return e.id +} + +// Get an item from the data map by key. +func (e *GenericEvent) Get(key string) interface{} { + if v, ok := e.data[key]; ok { + return v + } + + return nil +} + +// Set an item in the data map by key. +func (e *GenericEvent) Set(key string, val interface{}) { + if e.data == nil { + e.data = make(DataMap) + } + + e.data[key] = val +} + +// Data returns the entire data map. +func (e *GenericEvent) Data() DataMap { + return e.data +} + +// SetData overwrites the entire data map. +func (e *GenericEvent) SetData(data DataMap) { + if data != nil { + e.data = data + } +} + +// StopPropagation sets the event to no longer propagate +// to subsequent event listeners. +func (e *GenericEvent) StopPropagation(abort bool) { + e.propagationStopped = abort +} + +// IsPropagationStopped returns whether the event is set +// to no longer propagate. In other words, this is +// whether the event dispatcher should stop handling +// this event and skip passing it to any subsequent +// event listeners. +func (e *GenericEvent) IsPropagationStopped() bool { + return e.propagationStopped +} + +// Interface guards +var ( + _ Event = (*GenericEvent)(nil) +) diff --git a/modules/caddyevent/listener.go b/modules/caddyevent/listener.go index 637aad07245..8b29e897608 100644 --- a/modules/caddyevent/listener.go +++ b/modules/caddyevent/listener.go @@ -1,46 +1,46 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -// ListenerFunc is a function that can handle a dispatched event. -type ListenerFunc func(e Event) error - -// Handle runs the event listener on the given event. -func (fn ListenerFunc) Handle(e Event) error { - return fn(e) -} - -// Priority is a factor by which event listeners can be sorted. -type Priority int - -// ListenerEntry is a wrapper to allow associating a listener function -// to a priority factor when registering event subscribers and listeners. -type ListenerEntry struct { - Listener ListenerFunc - Priority Priority -} - -// Subscriber defines an interface for modules that wish -// to subscribe to dispatched events. -type Subscriber interface { - // SubscribedEvents returns a map of event IDs that - // this subscriber can handle, to the function that - // handles the event. - SubscribedEvents() map[caddy.ModuleID]ListenerEntry -} +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevent + +import ( + "github.com/caddyserver/caddy/v2" +) + +// ListenerFunc is a function that can handle a dispatched event. +type ListenerFunc func(e Event) error + +// Handle runs the event listener on the given event. +func (fn ListenerFunc) Handle(e Event) error { + return fn(e) +} + +// Priority is a factor by which event listeners can be sorted. +type Priority int + +// ListenerEntry is a wrapper to allow associating a listener function +// to a priority factor when registering event subscribers and listeners. +type ListenerEntry struct { + Listener ListenerFunc + Priority Priority +} + +// Subscriber defines an interface for modules that wish +// to subscribe to dispatched events. +type Subscriber interface { + // SubscribedEvents returns a map of event IDs that + // this subscriber can handle, to the function that + // handles the event. + SubscribedEvents() map[caddy.ModuleID]ListenerEntry +} diff --git a/modules/standard/imports.go b/modules/standard/imports.go index bc2d955df09..010742b597a 100644 --- a/modules/standard/imports.go +++ b/modules/standard/imports.go @@ -3,6 +3,7 @@ package standard import ( // standard Caddy modules _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + _ "github.com/caddyserver/caddy/v2/modules/caddyevent" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver" From 7df3ca7dac01011c8f2bcc0a73939cf86d5dcd3c Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sun, 31 Oct 2021 04:26:01 -0400 Subject: [PATCH 05/14] event: Hook into certmagic events --- modules/caddytls/automation.go | 1 + modules/caddytls/event.go | 93 ++++++++++++++++++++++++++++++++++ modules/caddytls/tls.go | 9 ++++ 3 files changed, 103 insertions(+) create mode 100644 modules/caddytls/event.go diff --git a/modules/caddytls/automation.go b/modules/caddytls/automation.go index 197c4095d28..bdbfddf8f3e 100644 --- a/modules/caddytls/automation.go +++ b/modules/caddytls/automation.go @@ -247,6 +247,7 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error { MustStaple: ap.MustStaple, RenewalWindowRatio: ap.RenewalWindowRatio, KeySource: keySource, + OnEvent: tlsApp.onEvent, OnDemand: ond, OCSP: certmagic.OCSPConfig{ DisableStapling: ap.DisableOCSPStapling, diff --git a/modules/caddytls/event.go b/modules/caddytls/event.go new file mode 100644 index 00000000000..7503cd25bcc --- /dev/null +++ b/modules/caddytls/event.go @@ -0,0 +1,93 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevent" + "github.com/caddyserver/certmagic" +) + +// onEvent translates certmagic events into caddy events +// then dispatches them asynchronously. +func (t *TLS) onEvent(event string, data interface{}) { + switch event { + // case "cached_managed_cert", "cached_unmanaged_cert": + // subjectNames, ok := data.([]string) + // if !ok { + // return + // } + // t.event.AsyncDispatch(caddyevent.NewGeneric( + // caddy.ModuleID("tls.event."+event), + // caddyevent.DataMap{"subjectNames": subjectNames}, + // )) + + // case "tls_handshake_started", "tls_handshake_completed": + // clientHello, ok := data.(*tls.ClientHelloInfo) + // if !ok { + // return + // } + // t.event.AsyncDispatch(NewHandshakeEvent(event, clientHello)) + + case "cert_obtained", "cert_renewed", "cert_revoked": + eventData, ok := data.(certmagic.CertificateEventData) + if !ok { + return + } + t.event.AsyncDispatch(NewCertEvent(event, eventData)) + } +} + +// CertEvent is dispatched when a certificate is obtained, renewed, or revoked. +type CertEvent struct { + caddyevent.GenericEvent +} + +func NewCertEvent(event string, data certmagic.CertificateEventData) *CertEvent { + certEvent := new(CertEvent) + certEvent.SetID(caddy.ModuleID("tls.event." + event)) + certEvent.Set("name", data.Name) + certEvent.Set("issuerKey", data.IssuerKey) + certEvent.Set("storageKey", data.StorageKey) + return certEvent +} + +func (e CertEvent) GetName() string { + return e.Get("name").(string) +} + +func (e CertEvent) GetIssuerKey() string { + return e.Get("issuerKey").(string) +} + +func (e CertEvent) GetStorageKey() string { + return e.Get("storageKey").(string) +} + +// HandshakeEvent is dispatched when a TLS handshake is started, or is completed. +// type HandshakeEvent struct { +// caddyevent.GenericEvent +// } + +// func NewHandshakeEvent(event string, clientHello *tls.ClientHelloInfo) *HandshakeEvent { +// handshakeEvent := new(HandshakeEvent) +// handshakeEvent.SetID(caddy.ModuleID("tls.event." + event)) +// handshakeEvent.Set("clientHello", clientHello) +// return handshakeEvent +// } + +// func (e CertEvent) GetClientHello() *tls.ClientHelloInfo { +// return e.Get("clientHello").(*tls.ClientHelloInfo) +// } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 429b24c9c1f..a310d194c5d 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -25,6 +25,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevent" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) @@ -73,6 +74,7 @@ type TLS struct { storageCleanTicker *time.Ticker storageCleanStop chan struct{} logger *zap.Logger + event *caddyevent.EventApp } // CaddyModule returns the Caddy module information. @@ -89,6 +91,12 @@ func (t *TLS) Provision(ctx caddy.Context) error { t.logger = ctx.Logger(t) repl := caddy.NewReplacer() + eventAppIface, err := ctx.App("event") + if err != nil { + return fmt.Errorf("getting event app: %v", err) + } + t.event = eventAppIface.(*caddyevent.EventApp) + // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { @@ -189,6 +197,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { magic := certmagic.New(t.certCache, certmagic.Config{ Storage: ctx.Storage(), Logger: t.logger, + OnEvent: t.onEvent, OCSP: certmagic.OCSPConfig{ DisableStapling: t.DisableOCSPStapling, }, From cae12f27d49b5c0c0b65de2430e48675f2f7a216 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Sat, 7 May 2022 14:31:46 -0400 Subject: [PATCH 06/14] event: Dispatch events on reverse proxy active health checks Addresses issue 4039 --- modules/caddyhttp/reverseproxy/event.go | 56 +++++++++++++++++++ .../caddyhttp/reverseproxy/healthchecks.go | 18 ++++-- .../caddyhttp/reverseproxy/reverseproxy.go | 8 +++ 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 modules/caddyhttp/reverseproxy/event.go diff --git a/modules/caddyhttp/reverseproxy/event.go b/modules/caddyhttp/reverseproxy/event.go new file mode 100644 index 00000000000..2ab39fdb308 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/event.go @@ -0,0 +1,56 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverseproxy + +import ( + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevent" +) + +// ActiveUnhealthyEvent is dispatched when an upstream +// became unhealthy via active health checks, when it was +// previously healthy. +type ActiveUnhealthyEvent struct { + caddyevent.GenericEvent +} + +func NewActiveUnhealthyEvent(hostAddr string) *ActiveUnhealthyEvent { + event := new(ActiveUnhealthyEvent) + event.SetID(caddy.ModuleID("http.handlers.reverse_proxy.event.active_unhealthy")) + event.Set("host", hostAddr) + return event +} + +func (e ActiveUnhealthyEvent) GetHost() string { + return e.Get("host").(string) +} + +// ActiveUnhealthyEvent is dispatched when an upstream +// became healthy via active health checks, when it was +// previously unhealthy. +type ActiveHealthyEvent struct { + caddyevent.GenericEvent +} + +func NewActiveHealthyEvent(hostAddr string) *ActiveHealthyEvent { + event := new(ActiveHealthyEvent) + event.SetID(caddy.ModuleID("http.handlers.reverse_proxy.event.active_healthy")) + event.Set("host", hostAddr) + return event +} + +func (e ActiveHealthyEvent) GetHost() string { + return e.Get("host").(string) +} diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 317b283ecd8..c47b5bd6552 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -284,6 +284,13 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre } } + markUnhealthy := func() { + // dispatch an event that the host newly became unhealthy + if upstream.setHealthy(false) { + h.event.AsyncDispatch(NewActiveUnhealthyEvent(hostAddr)) + } + } + // do the request, being careful to tame the response body resp, err := h.HealthChecks.Active.httpClient.Do(req) if err != nil { @@ -291,7 +298,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.String("host", hostAddr), zap.Error(err), ) - upstream.setHealthy(false) + markUnhealthy() return nil } var body io.Reader = resp.Body @@ -311,7 +318,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.Int("status_code", resp.StatusCode), zap.String("host", hostAddr), ) - upstream.setHealthy(false) + markUnhealthy() return nil } } else if resp.StatusCode < 200 || resp.StatusCode >= 400 { @@ -319,7 +326,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.Int("status_code", resp.StatusCode), zap.String("host", hostAddr), ) - upstream.setHealthy(false) + markUnhealthy() return nil } @@ -331,14 +338,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre zap.String("host", hostAddr), zap.Error(err), ) - upstream.setHealthy(false) + markUnhealthy() return nil } if !h.HealthChecks.Active.bodyRegexp.Match(bodyBytes) { h.HealthChecks.Active.logger.Info("response body failed expectations", zap.String("host", hostAddr), ) - upstream.setHealthy(false) + markUnhealthy() return nil } } @@ -346,6 +353,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // passed health check parameters, so mark as healthy if upstream.setHealthy(true) { h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) + h.event.AsyncDispatch(NewActiveHealthyEvent(hostAddr)) } return nil diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 7061275094d..07eea31507e 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -33,6 +33,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyevent" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" @@ -179,6 +180,7 @@ type Handler struct { ctx caddy.Context logger *zap.Logger + event *caddyevent.EventApp } // CaddyModule returns the Caddy module information. @@ -194,6 +196,12 @@ func (h *Handler) Provision(ctx caddy.Context) error { h.ctx = ctx h.logger = ctx.Logger(h) + eventAppIface, err := ctx.App("event") + if err != nil { + return fmt.Errorf("getting event app: %v", err) + } + h.event = eventAppIface.(*caddyevent.EventApp) + // verify SRV compatibility - TODO: LookupSRV deprecated; will be removed for i, v := range h.Upstreams { if v.LookupSRV == "" { From 2ee7697a7f033a5dd69125d6ac4eac0cd60f96d7 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 25 Jul 2022 16:01:04 -0400 Subject: [PATCH 07/14] Fix lint --- modules/caddyevent/app.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/caddyevent/app.go b/modules/caddyevent/app.go index 5411ef99673..59e4c18f251 100644 --- a/modules/caddyevent/app.go +++ b/modules/caddyevent/app.go @@ -146,7 +146,7 @@ func (app *EventApp) Dispatch(event Event) error { // listeners may mark the event to stop subsequent // listeners from running on this event. if event.IsPropagationStopped() { - app.logger.Debug("propogation stopped", zap.String("event", event.ID().Name())) + app.logger.Debug("propagation stopped", zap.String("event", event.ID().Name())) break } @@ -186,9 +186,7 @@ func (app *EventApp) optimizeListeners() { // add all the listeners in order of priority for _, k := range keys { - for _, listener := range priorities[Priority(k)] { - app.optimized[eventID] = append(app.optimized[eventID], listener) - } + app.optimized[eventID] = append(app.optimized[eventID], priorities[Priority(k)]...) } } From 10a2b1e25b127dfa4b2615b13df07664a178af0f Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Thu, 25 Aug 2022 16:37:31 -0600 Subject: [PATCH 08/14] events: Begin implementing event system (#4984) --- context.go | 68 +++- modules/caddyevent/app.go | 202 ---------- modules/caddyevent/event.go | 140 ------- modules/caddyevent/listener.go | 46 --- modules/caddyevents/app.go | 358 ++++++++++++++++++ modules/caddyevents/eventsconfig/caddyfile.go | 88 +++++ modules/caddyevents/exec.go | 88 +++++ modules/caddyhttp/app.go | 8 + modules/caddyhttp/reverseproxy/event.go | 56 --- .../caddyhttp/reverseproxy/healthchecks.go | 4 +- .../caddyhttp/reverseproxy/reverseproxy.go | 15 +- modules/caddyhttp/server.go | 3 + modules/caddytls/event.go | 93 ----- modules/caddytls/tls.go | 32 +- modules/standard/imports.go | 3 +- 15 files changed, 632 insertions(+), 572 deletions(-) delete mode 100644 modules/caddyevent/app.go delete mode 100644 modules/caddyevent/event.go delete mode 100644 modules/caddyevent/listener.go create mode 100644 modules/caddyevents/app.go create mode 100644 modules/caddyevents/eventsconfig/caddyfile.go create mode 100644 modules/caddyevents/exec.go delete mode 100644 modules/caddyhttp/reverseproxy/event.go delete mode 100644 modules/caddytls/event.go diff --git a/context.go b/context.go index 2a6f514214a..a2506a75b2a 100644 --- a/context.go +++ b/context.go @@ -37,9 +37,10 @@ import ( // not actually need to do this). type Context struct { context.Context - moduleInstances map[string][]interface{} + moduleInstances map[string][]Module cfg *Config cleanupFuncs []func() + ancestry []Module } // NewContext provides a new context derived from the given @@ -51,7 +52,7 @@ type Context struct { // modules which are loaded will be properly unloaded. // See standard library context package's documentation. func NewContext(ctx Context) (Context, context.CancelFunc) { - newCtx := Context{moduleInstances: make(map[string][]interface{}), cfg: ctx.cfg} + newCtx := Context{moduleInstances: make(map[string][]Module), cfg: ctx.cfg} c, cancel := context.WithCancel(ctx.Context) wrappedCancel := func() { cancel() @@ -90,15 +91,15 @@ func (ctx *Context) OnCancel(f func()) { // ModuleMap may be used in place of map[string]json.RawMessage. The return value's // underlying type mirrors the input field's type: // -// json.RawMessage => interface{} -// []json.RawMessage => []interface{} -// [][]json.RawMessage => [][]interface{} -// map[string]json.RawMessage => map[string]interface{} -// []map[string]json.RawMessage => []map[string]interface{} +// json.RawMessage => interface{} +// []json.RawMessage => []interface{} +// [][]json.RawMessage => [][]interface{} +// map[string]json.RawMessage => map[string]interface{} +// []map[string]json.RawMessage => []map[string]interface{} // // The field must have a "caddy" struct tag in this format: // -// caddy:"key1=val1 key2=val2" +// caddy:"key1=val1 key2=val2" // // To load modules, a "namespace" key is required. For example, to load modules // in the "http.handlers" namespace, you'd put: `namespace=http.handlers` in the @@ -115,7 +116,7 @@ func (ctx *Context) OnCancel(f func()) { // meaning the key containing the module's name that is defined inline with the module // itself. You must specify the inline key in a struct tag, along with the namespace: // -// caddy:"namespace=http.handlers inline_key=handler" +// caddy:"namespace=http.handlers inline_key=handler" // // This will look for a key/value pair like `"handler": "..."` in the json.RawMessage // in order to know the module name. @@ -301,17 +302,17 @@ func (ctx Context) loadModuleMap(namespace string, val reflect.Value) (map[strin // like from embedded scripts, etc. func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{}, error) { modulesMu.RLock() - mod, ok := modules[id] + modInfo, ok := modules[id] modulesMu.RUnlock() if !ok { return nil, fmt.Errorf("unknown module: %s", id) } - if mod.New == nil { - return nil, fmt.Errorf("module '%s' has no constructor", mod.ID) + if modInfo.New == nil { + return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID) } - val := mod.New().(interface{}) + val := modInfo.New() // value must be a pointer for unmarshaling into concrete type, even if // 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) (interface{ if len(rawMsg) > 0 { err := strictUnmarshalJSON(rawMsg, &val) if err != nil { - return nil, fmt.Errorf("decoding module config: %s: %v", mod, err) + return nil, fmt.Errorf("decoding module config: %s: %v", modInfo, err) } } @@ -340,6 +341,8 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ return nil, fmt.Errorf("module value cannot be null") } + ctx.ancestry = append(ctx.ancestry, val) + if prov, ok := val.(Provisioner); ok { err := prov.Provision(ctx) if err != nil { @@ -351,7 +354,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } - return nil, fmt.Errorf("provision %s: %v", mod, err) + return nil, fmt.Errorf("provision %s: %v", modInfo, err) } } @@ -365,7 +368,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (interface{ err = fmt.Errorf("%v; additionally, cleanup: %v", err, err2) } } - return nil, fmt.Errorf("%s: invalid configuration: %v", mod, err) + return nil, fmt.Errorf("%s: invalid configuration: %v", modInfo, err) } } @@ -439,8 +442,10 @@ func (ctx Context) Storage() certmagic.Storage { return ctx.cfg.storage } +// TODO: aw man, can I please change this? // Logger returns a logger that can be used by mod. func (ctx Context) Logger(mod Module) *zap.Logger { + // TODO: if mod is nil, use ctx.Module() instead... if ctx.cfg == nil { // often the case in tests; just use a dev logger l, err := zap.NewDevelopment() @@ -451,3 +456,34 @@ func (ctx Context) Logger(mod Module) *zap.Logger { } return ctx.cfg.Logging.Logger(mod) } + +// TODO: use this +// // Logger returns a logger that can be used by the current module. +// func (ctx Context) Log() *zap.Logger { +// if ctx.cfg == nil { +// // often the case in tests; just use a dev logger +// l, err := zap.NewDevelopment() +// if err != nil { +// panic("config missing, unable to create dev logger: " + err.Error()) +// } +// return l +// } +// return ctx.cfg.Logging.Logger(ctx.Module()) +// } + +// Modules returns the lineage of modules that this context provisioned, +// with the most recent/current module being last in the list. +func (ctx Context) Modules() []Module { + mods := make([]Module, len(ctx.ancestry)) + copy(mods, ctx.ancestry) + return mods +} + +// Module returns the current module, or the most recent one +// provisioned by the context. +func (ctx Context) Module() Module { + if len(ctx.ancestry) == 0 { + return nil + } + return ctx.ancestry[len(ctx.ancestry)-1] +} diff --git a/modules/caddyevent/app.go b/modules/caddyevent/app.go deleted file mode 100644 index 59e4c18f251..00000000000 --- a/modules/caddyevent/app.go +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "encoding/json" - "fmt" - "sort" - - "github.com/caddyserver/caddy/v2" - "go.uber.org/zap" -) - -func init() { - caddy.RegisterModule(EventApp{}) -} - -// EventApp is a global event system. -type EventApp struct { - // Registers each of these event subscribers - SubscribersRaw []json.RawMessage `json:"subscribers,omitempty" caddy:"namespace=event.subscribers inline_key=subscriber"` - - listeners map[caddy.ModuleID]map[Priority][]ListenerFunc - optimized map[caddy.ModuleID][]ListenerFunc - ready bool - logger *zap.Logger -} - -// CaddyModule returns the Caddy module information. -func (EventApp) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "event", - New: func() caddy.Module { return new(EventApp) }, - } -} - -// Provision sets up the app. -func (app *EventApp) Provision(ctx caddy.Context) error { - app.listeners = make(map[caddy.ModuleID]map[Priority][]ListenerFunc) - app.logger = ctx.Logger(app) - - // register all the configured subscribers - if app.SubscribersRaw != nil { - subscribersIface, err := ctx.LoadModule(app, "SubscribersRaw") - if err != nil { - return fmt.Errorf("loading event subscriber modules: %v", err) - } - for _, subscriber := range subscribersIface.([]Subscriber) { - app.RegisterSubscriber(subscriber) - } - } - - return nil -} - -// Validate ensures the app's configuration is valid. -func (app *EventApp) Validate() error { - return nil -} - -// Start runs the app. -func (app *EventApp) Start() error { - // optimize the event listeners to order them by priority. - app.optimizeListeners() - - // stop new listeners from being added, - // and allow events to be dispatched. - app.ready = true - - return nil -} - -// Stop gracefully shuts down the app. -func (app *EventApp) Stop() error { - return nil -} - -// RegisterSubscriber registers all the listeners from a subscriber. -// Modules may register themselves as subscribers during their Provision -// phase. Subscribers cannot be registered after the config is running. -func (app *EventApp) RegisterSubscriber(subscriber Subscriber) { - for eventID, entry := range subscriber.SubscribedEvents() { - app.RegisterListener(eventID, entry) - } -} - -// RegisterListener registers a single event listener. -// Modules may register listeners during their Provision phase. -// Listeners cannot be registered after the config is running. -func (app *EventApp) RegisterListener(eventID caddy.ModuleID, entry ListenerEntry) { - // if the app is already running, we don't allow adding new listeners. - if app.ready { - // TODO: Panic or something? - return - } - - if app.listeners[eventID] == nil { - app.listeners[eventID] = make(map[Priority][]ListenerFunc) - } - - // There may be more than one listener with the same priority, - // for the same event, so we have an array of listeners for - // each priority level. Listeners at the same priority level - // will be in the order they are registered, which will not - // have a guaranteed order because Caddy modules may be loaded - // in an arbitrary order. - app.listeners[eventID][entry.Priority] = append( - app.listeners[eventID][entry.Priority], - entry.Listener, - ) - - app.logger.Debug("registered listener", - zap.String("event", eventID.Name()), - zap.Int("priority", int(entry.Priority)), - ) -} - -// Dispatch passes the event through the configured listeners synchronously. -func (app *EventApp) Dispatch(event Event) error { - // if the app is not running, we don't allow dispatching events. - if !app.ready { - return fmt.Errorf("Cannot dispatch events until after the app is running") - } - - // find the listeners for this event - listeners, ok := app.optimized[event.ID()] - if !ok { - return nil - } - - app.logger.Debug("dispatching", zap.String("event", event.ID().Name())) - - for _, listener := range listeners { - // listeners may mark the event to stop subsequent - // listeners from running on this event. - if event.IsPropagationStopped() { - app.logger.Debug("propagation stopped", zap.String("event", event.ID().Name())) - break - } - - // run the listener. - err := listener(event) - if err != nil { - app.logger.Error("listener error", - zap.String("event", event.ID().Name()), - zap.Error(err), - ) - return err - } - } - - return nil -} - -// AsyncDispatch passes the event through the configured listeners asynchronously. -func (app *EventApp) AsyncDispatch(event Event) { - go func(event Event) { - _ = app.Dispatch(event) - }(event) -} - -// optimizeListeners orders the listeners by priority. -func (app *EventApp) optimizeListeners() { - app.optimized = make(map[caddy.ModuleID][]ListenerFunc) - for eventID, priorities := range app.listeners { - app.optimized[eventID] = []ListenerFunc{} - - // sort the priorities (highest priority value first) - keys := make([]int, 0) - for k := range priorities { - keys = append(keys, int(k)) - } - sort.Sort(sort.Reverse(sort.IntSlice(keys))) - - // add all the listeners in order of priority - for _, k := range keys { - app.optimized[eventID] = append(app.optimized[eventID], priorities[Priority(k)]...) - } - } - - // we no longer need this map once we're running - app.listeners = nil -} - -// Interface guards -var ( - _ caddy.App = (*EventApp)(nil) - _ caddy.Provisioner = (*EventApp)(nil) - _ caddy.Validator = (*EventApp)(nil) -) diff --git a/modules/caddyevent/event.go b/modules/caddyevent/event.go deleted file mode 100644 index 6255e5e6b66..00000000000 --- a/modules/caddyevent/event.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -// DataMap is a key-value pair map. -type DataMap map[string]interface{} - -// Event interface -type Event interface { - // Get the module ID of the event. - ID() caddy.ModuleID - - // Get an item from the data map by key. - Get(key string) interface{} - - // Set an item in the data map by key. - Set(key string, val interface{}) - - // Get the entire data map. - Data() DataMap - - // Replace the entire data map. - SetData(DataMap) - - // StopPropagation sets the event to no longer propagate - // to subsequent event listeners. - StopPropagation(bool) - - // IsPropagationStopped returns whether the event is set - // to no longer propagate. In other words, this is - // whether the event dispatcher should stop handling - // this event and skip passing it to any subsequent - // event listeners. - IsPropagationStopped() bool -} - -// GenericEvent a generic event, which can be used -// with composition to provide baseline functionality -// for custom events. -type GenericEvent struct { - // The module ID of the event. - id caddy.ModuleID - - // Key-value data pairs of contextual information. - data DataMap - - // Whether the event handling should be aborted, - // and no event handlers should be called after - // this point. Allows for "middleware chain" type - // of functionality. - propagationStopped bool -} - -// NewGeneric creates a generic event instance. -func NewGeneric(id caddy.ModuleID, data DataMap) *GenericEvent { - if data == nil { - data = make(DataMap) - } - - return &GenericEvent{ - id: id, - data: data, - } -} - -// SetID sets the module ID of the event. -func (e *GenericEvent) SetID(id caddy.ModuleID) { - e.id = id -} - -// Get the module ID of the event. -func (e *GenericEvent) ID() caddy.ModuleID { - return e.id -} - -// Get an item from the data map by key. -func (e *GenericEvent) Get(key string) interface{} { - if v, ok := e.data[key]; ok { - return v - } - - return nil -} - -// Set an item in the data map by key. -func (e *GenericEvent) Set(key string, val interface{}) { - if e.data == nil { - e.data = make(DataMap) - } - - e.data[key] = val -} - -// Data returns the entire data map. -func (e *GenericEvent) Data() DataMap { - return e.data -} - -// SetData overwrites the entire data map. -func (e *GenericEvent) SetData(data DataMap) { - if data != nil { - e.data = data - } -} - -// StopPropagation sets the event to no longer propagate -// to subsequent event listeners. -func (e *GenericEvent) StopPropagation(abort bool) { - e.propagationStopped = abort -} - -// IsPropagationStopped returns whether the event is set -// to no longer propagate. In other words, this is -// whether the event dispatcher should stop handling -// this event and skip passing it to any subsequent -// event listeners. -func (e *GenericEvent) IsPropagationStopped() bool { - return e.propagationStopped -} - -// Interface guards -var ( - _ Event = (*GenericEvent)(nil) -) diff --git a/modules/caddyevent/listener.go b/modules/caddyevent/listener.go deleted file mode 100644 index 8b29e897608..00000000000 --- a/modules/caddyevent/listener.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevent - -import ( - "github.com/caddyserver/caddy/v2" -) - -// ListenerFunc is a function that can handle a dispatched event. -type ListenerFunc func(e Event) error - -// Handle runs the event listener on the given event. -func (fn ListenerFunc) Handle(e Event) error { - return fn(e) -} - -// Priority is a factor by which event listeners can be sorted. -type Priority int - -// ListenerEntry is a wrapper to allow associating a listener function -// to a priority factor when registering event subscribers and listeners. -type ListenerEntry struct { - Listener ListenerFunc - Priority Priority -} - -// Subscriber defines an interface for modules that wish -// to subscribe to dispatched events. -type Subscriber interface { - // SubscribedEvents returns a map of event IDs that - // this subscriber can handle, to the function that - // handles the event. - SubscribedEvents() map[caddy.ModuleID]ListenerEntry -} diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go new file mode 100644 index 00000000000..761ea221894 --- /dev/null +++ b/modules/caddyevents/app.go @@ -0,0 +1,358 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevents + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/google/uuid" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(App{}) +} + +// App implements a global eventing system within Caddy. +// Modules can emit and subscribe to events, providing +// hooks into deep parts of the code base that aren't +// otherwise accessible. Events provide information about +// what and when things are happening, and this facility +// allows handlers to take action when events occur, +// add information to the event's metadata, and even +// control program flow in some cases. +// +// Events are propagated in a DOM-like fashion. An event +// event emitted from module `a.b.c` (the "origin") will +// first invoke handlers listening to `a.b.c`, then `a.b`, +// then `a`, then those listening regardless of origin. +// If a handler returns the special error Aborted, then +// propagation immediately stops and the event is marked +// as aborted. Emitters may optionally choose to adjust +// program flow based on an abort. +// +// Modules can subscribe to events by origin and/or name. +// A handler is invoked only if it is subscribed to the +// event by name and origin. Subscriptions should be +// registered during the provisioning phase, before apps +// are started. +// +// Event handlers are fired synchronously as part of the +// regular flow of the program. This allows event handlers +// to control the flow of the program if the origin permits +// it and also allows handlers to convey new information +// back into the origin module before it continues. +// In essence, event handlers are similar to HTTP +// middleware handlers. +// +// Event bindings/subscribers are unordered; i.e. +// event handlers are invoked in an arbitrary order. +// Event handlers should not rely on the logic of other +// handlers to succeed. +type App struct { + // Subscriptions bind handlers to one or more events + // either globally or scoped to specific modules or module + // namespaces. + Subscriptions []*Subscription `json:"subscriptions,omitempty"` + + // Map of event name to map of module ID/namespace to handlers + subscriptions map[string]map[caddy.ModuleID][]Handler + + logger *zap.Logger + started bool +} + +// Subscription represents binding of one or more handlers to +// one or more events. +type Subscription struct { + // The name(s) of the event(s) to bind to. Default: all events. + Events []string `json:"events,omitempty"` + + // The ID or namespace of the module(s) from which events + // originate to listen to for events. Default: all modules. + // Events propagate up, so events emitted by module "a.b.c" + // will also trigger the event for "a.b" and "a". Thus, to + // receive all events from "a.b.c" and "a.b.d", for example, + // one can subscribe to either "a.b" or all of "a" entirely. + Modules []caddy.ModuleID `json:"modules,omitempty"` + + // The event handler modules. These implement the actual + // behavior to invoke when an event occurs. At least one + // handler is required. + HandlersRaw []json.RawMessage `json:"handlers,omitempty" caddy:"namespace=events.handlers inline_key=handler"` + + // The decoded handlers; Go code that is subscribing to + // an event should set this field directly; HandlersRaw + // is meant for JSON configuration to fill out this field. + Handlers []Handler `json:"-"` +} + +// CaddyModule returns the Caddy module information. +func (App) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "events", + New: func() caddy.Module { return new(App) }, + } +} + +// Provision sets up the app. +func (app *App) Provision(ctx caddy.Context) error { + app.logger = ctx.Logger(app) + app.subscriptions = make(map[string]map[caddy.ModuleID][]Handler) + + for _, sub := range app.Subscriptions { + if sub.HandlersRaw != nil { + handlersIface, err := ctx.LoadModule(sub, "HandlersRaw") + if err != nil { + return fmt.Errorf("loading event subscriber modules: %v", err) + } + for _, h := range handlersIface.([]interface{}) { + sub.Handlers = append(sub.Handlers, h.(Handler)) + } + if len(sub.Handlers) == 0 { + // pointless to bind without any handlers + return fmt.Errorf("no handlers defined") + } + } + } + + return nil +} + +// Start runs the app. +func (app *App) Start() error { + for _, sub := range app.Subscriptions { + if err := app.Subscribe(sub); err != nil { + return err + } + } + + app.started = true + + return nil +} + +// Stop gracefully shuts down the app. +func (app *App) Stop() error { + return nil +} + +// Subscribe binds one or more event handlers to one or more events +// according to the subscription s. For now, subscriptions can only +// be created during the provision phase; new bindings cannot be +// created after the events app has started. +func (app *App) Subscribe(s *Subscription) error { + if app.started { + return fmt.Errorf("events already started; new subscriptions closed") + } + + // handle special case of catch-alls (omission of event name or module space implies all) + if len(s.Events) == 0 { + s.Events = []string{""} + } + if len(s.Modules) == 0 { + s.Modules = []caddy.ModuleID{""} + } + + for _, eventName := range s.Events { + if app.subscriptions[eventName] == nil { + app.subscriptions[eventName] = make(map[caddy.ModuleID][]Handler) + } + for _, originModule := range s.Modules { + app.subscriptions[eventName][originModule] = append(app.subscriptions[eventName][originModule], s.Handlers...) + } + } + + return nil +} + +// On is syntactic sugar for Subscribe() that binds a single handler +// to a single event from any module. If the eventName is empty string, +// it counts for all events. +func (app *App) On(eventName string, handler Handler) error { + return app.Subscribe(&Subscription{ + Events: []string{eventName}, + Handlers: []Handler{handler}, + }) +} + +// Emit creates and dispatches an event named eventName to all relevant handlers with +// the metadata data. Events are emitted and propagated synchronously. The returned Event +// value will have any additional information from the invoked handlers. +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]interface{}) Event { + id, err := uuid.NewRandom() + if err != nil { + app.logger.Error("failed generating new event ID", zap.Error(err)) + } + + eventName = strings.ToLower(eventName) + + // TODO: make pointer? + e := Event{ + id: id, + ts: time.Now(), + name: eventName, + origin: ctx.Module(), + data: data, + } + + // add event info to replacer, make sure it's in the context + repl, ok := ctx.Context.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + if !ok { + repl = caddy.NewReplacer() + ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl) + } + repl.Set("event", e) + repl.Set("event.id", e.id) + repl.Set("event.name", e.name) + repl.Set("event.module", e.origin.CaddyModule().ID) + repl.Set("event.data", e.data) + repl.Map(func(key string) (interface{}, bool) { + switch key { + case "event.time": + return e.ts, true + case "event.time_unix": + return e.ts.UnixMilli(), true + } + + if strings.HasPrefix(key, "event.data.") { + key = strings.TrimPrefix(key, "event.data.") + if val, ok := data[key]; ok { + return val, true + } + } + + return nil, false + }) + + app.logger.Debug("event", + zap.String("name", e.name), + zap.String("id", e.id.String()), + zap.String("origin", e.origin.CaddyModule().String()), + zap.Any("data", e.data), + ) + + // invoke handlers bound to the event by name and also all events; this for loop + // iterates twice at most: once for the event name, once for "" (all events) + for { + moduleID := e.origin.CaddyModule().ID + + // implement propagation up the module tree (i.e. start with "a.b.c" then "a.b" then "a" then "") + for { + if app.subscriptions[eventName] == nil { + break // shortcut if event not bound at all + } + + for _, handler := range app.subscriptions[eventName][moduleID] { + // TODO: maybe log handler invocations instead of all event emissions? + err := handler.Handle(ctx, e) + if errors.Is(err, ErrAborted) { + // TODO: Figure out proper way to implement this + e.Aborted = err + return e + } + if err != nil { + app.logger.Error("handler error", zap.Error(err)) + } + } + + if moduleID == "" { + break + } + lastDot := strings.LastIndex(string(moduleID), ".") + if lastDot < 0 { + moduleID = "" // include handlers bound to events regardless of module + } else { + moduleID = moduleID[:lastDot] + } + } + + // include handlers listening to all events + if eventName == "" { + break + } + eventName = "" + } + + return e +} + +// Event represents something that has happened or is happening. +type Event struct { + id uuid.UUID + ts time.Time + name string + origin caddy.Module + data map[string]interface{} + + // If non-nil, the event has been aborted, meaning + // propagation has stopped to other handlers and + // the code should stop what it was doing. Emitters + // may choose to use this as a signal to adjust their + // code path appropriately. + Aborted error +} + +// CloudEvent exports event e as a structure that, when +// serialized as JSON, is compatible with the +// CloudEvents spec. +func (e Event) CloudEvent() CloudEvent { + dataJSON, _ := json.Marshal(e.data) + return CloudEvent{ + ID: e.id.String(), + Source: e.origin.CaddyModule().String(), + SpecVersion: "1.0", + Type: e.name, + Time: e.ts, + DataContentType: "application/json", + Data: dataJSON, + } +} + +// CloudEvent is a JSON-serializable structure that +// is compatible with the CloudEvents specification. +// See https://cloudevents.io. +type CloudEvent struct { + ID string `json:"id"` + Source string `json:"source"` + SpecVersion string `json:"specversion"` + Type string `json:"type"` + Time time.Time `json:"time"` + DataContentType string `json:"datacontenttype,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +// ErrAborted cancels an event. +var ErrAborted = errors.New("event aborted") + +// Handler is a type that can handle events. +type Handler interface { + Handle(context.Context, Event) error +} + +// // HandlerFunc +// type HandlerFunc func(context.Context, Event) error + +// Interface guards +var ( + _ caddy.App = (*App)(nil) + _ caddy.Provisioner = (*App)(nil) +) diff --git a/modules/caddyevents/eventsconfig/caddyfile.go b/modules/caddyevents/eventsconfig/caddyfile.go new file mode 100644 index 00000000000..6b1347d6cba --- /dev/null +++ b/modules/caddyevents/eventsconfig/caddyfile.go @@ -0,0 +1,88 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package eventsconfig is for configuring caddyevents.App with the +// Caddyfile. This code can't be in the caddyevents package because +// the httpcaddyfile package imports caddyhttp, which imports +// caddyevents: hence, it creates an import cycle. +package eventsconfig + +import ( + "encoding/json" + + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyevents" +) + +func init() { + httpcaddyfile.RegisterGlobalOption("events", parseApp) +} + +// parseApp configures the "events" global option from Caddyfile to set up the events app. +// Syntax: +// +// events { +// on +// } +// +// If is *, then it will bind to all events. +func parseApp(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { + app := new(caddyevents.App) + + // consume the option name + if !d.Next() { + return nil, d.ArgErr() + } + + // handle the block + for d.NextBlock(0) { + switch d.Val() { + case "on": + if !d.NextArg() { + return nil, d.ArgErr() + } + eventName := d.Val() + if eventName == "*" { + eventName = "" + } + + if !d.NextArg() { + return nil, d.ArgErr() + } + handlerName := d.Val() + modID := "events.handlers." + handlerName + unm, err := caddyfile.UnmarshalModule(d, modID) + if err != nil { + return nil, err + } + + app.Subscriptions = append(app.Subscriptions, &caddyevents.Subscription{ + Events: []string{eventName}, + HandlersRaw: []json.RawMessage{ + caddyconfig.JSONModuleObject(unm, "handler", handlerName, nil), + }, + }) + + default: + return nil, d.ArgErr() + } + } + + return httpcaddyfile.App{ + Name: "events", + Value: caddyconfig.JSON(app, nil), + }, nil +} diff --git a/modules/caddyevents/exec.go b/modules/caddyevents/exec.go new file mode 100644 index 00000000000..d5962e73d5b --- /dev/null +++ b/modules/caddyevents/exec.go @@ -0,0 +1,88 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddyevents + +import ( + "context" + "os" + "os/exec" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "go.uber.org/zap" +) + +func init() { + caddy.RegisterModule(ExecHandler{}) +} + +// ExecHandler implements an event handler that runs a command/program. +type ExecHandler struct { + Command string `json:"command,omitempty"` + Args []string `json:"args,omitempty"` + Dir string `json:"dir,omitempty"` + Timeout caddy.Duration `json:"timeout,omitempty"` + + logger *zap.Logger +} + +// CaddyModule returns the Caddy module information. +func (ExecHandler) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "events.handlers.exec", + New: func() caddy.Module { return new(ExecHandler) }, + } +} + +// Provision sets up the module. +func (eh *ExecHandler) Provision(ctx caddy.Context) error { + eh.logger = ctx.Logger(eh) + return nil +} + +func (eh *ExecHandler) Handle(ctx context.Context, e Event) error { + repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + + // expand placeholders in command args; + // notably, WE DO NOT EXPAND PLACEHOLDERS + // IN THE COMMAND ITSELF for safety reasons + expandedArgs := make([]string, len(eh.Args)) + for i := range eh.Args { + expandedArgs[i] = repl.ReplaceAll(eh.Args[i], "") + } + + cmd := exec.Command(eh.Command, expandedArgs...) + cmd.Dir = eh.Dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +func (eh *ExecHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + return d.ArgErr() + } + eh.Command = d.Val() + eh.Args = d.RemainingArgs() + } + return nil +} + +// Interface guards +var ( + _ caddyfile.Unmarshaler = (*ExecHandler)(nil) + _ caddy.Provisioner = (*ExecHandler)(nil) +) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index 1894a975ce4..603108aa62f 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -23,6 +23,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/lucas-clemente/quic-go/http3" "go.uber.org/zap" @@ -146,6 +147,11 @@ func (app *App) Provision(ctx caddy.Context) error { app.ctx = ctx app.logger = ctx.Logger(app) + eventsAppIface, err := ctx.App("events") + if err != nil { + return fmt.Errorf("getting events app: %v", err) + } + repl := caddy.NewReplacer() // this provisions the matchers for each route, @@ -160,6 +166,8 @@ func (app *App) Provision(ctx caddy.Context) error { for srvName, srv := range app.Servers { srv.name = srvName srv.tlsApp = app.tlsApp + srv.events = eventsAppIface.(*caddyevents.App) + srv.ctx = ctx srv.logger = app.logger.Named("log") srv.errorLogger = app.logger.Named("log.error") diff --git a/modules/caddyhttp/reverseproxy/event.go b/modules/caddyhttp/reverseproxy/event.go deleted file mode 100644 index 2ab39fdb308..00000000000 --- a/modules/caddyhttp/reverseproxy/event.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package reverseproxy - -import ( - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyevent" -) - -// ActiveUnhealthyEvent is dispatched when an upstream -// became unhealthy via active health checks, when it was -// previously healthy. -type ActiveUnhealthyEvent struct { - caddyevent.GenericEvent -} - -func NewActiveUnhealthyEvent(hostAddr string) *ActiveUnhealthyEvent { - event := new(ActiveUnhealthyEvent) - event.SetID(caddy.ModuleID("http.handlers.reverse_proxy.event.active_unhealthy")) - event.Set("host", hostAddr) - return event -} - -func (e ActiveUnhealthyEvent) GetHost() string { - return e.Get("host").(string) -} - -// ActiveUnhealthyEvent is dispatched when an upstream -// became healthy via active health checks, when it was -// previously unhealthy. -type ActiveHealthyEvent struct { - caddyevent.GenericEvent -} - -func NewActiveHealthyEvent(hostAddr string) *ActiveHealthyEvent { - event := new(ActiveHealthyEvent) - event.SetID(caddy.ModuleID("http.handlers.reverse_proxy.event.active_healthy")) - event.Set("host", hostAddr) - return event -} - -func (e ActiveHealthyEvent) GetHost() string { - return e.Get("host").(string) -} diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index c47b5bd6552..bbd81ccacda 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -287,7 +287,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre markUnhealthy := func() { // dispatch an event that the host newly became unhealthy if upstream.setHealthy(false) { - h.event.AsyncDispatch(NewActiveUnhealthyEvent(hostAddr)) + h.events.Emit(h.ctx, "unhealthy", map[string]interface{}{"host": hostAddr}) } } @@ -353,7 +353,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // passed health check parameters, so mark as healthy if upstream.setHealthy(true) { h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) - h.event.AsyncDispatch(NewActiveHealthyEvent(hostAddr)) + h.events.Emit(h.ctx, "healthy", map[string]interface{}{"host": hostAddr}) } return nil diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go index 07eea31507e..91d2fdd352d 100644 --- a/modules/caddyhttp/reverseproxy/reverseproxy.go +++ b/modules/caddyhttp/reverseproxy/reverseproxy.go @@ -33,7 +33,7 @@ import ( "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/caddyserver/caddy/v2/modules/caddyevent" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddyhttp" "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers" "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" @@ -180,7 +180,7 @@ type Handler struct { ctx caddy.Context logger *zap.Logger - event *caddyevent.EventApp + events *caddyevents.App } // CaddyModule returns the Caddy module information. @@ -193,14 +193,13 @@ func (Handler) CaddyModule() caddy.ModuleInfo { // Provision ensures that h is set up properly before use. func (h *Handler) Provision(ctx caddy.Context) error { - h.ctx = ctx - h.logger = ctx.Logger(h) - - eventAppIface, err := ctx.App("event") + eventAppIface, err := ctx.App("events") if err != nil { - return fmt.Errorf("getting event app: %v", err) + return fmt.Errorf("getting events app: %v", err) } - h.event = eventAppIface.(*caddyevent.EventApp) + h.events = eventAppIface.(*caddyevents.App) + h.ctx = ctx + h.logger = ctx.Logger(h) // verify SRV compatibility - TODO: LookupSRV deprecated; will be removed for i, v := range h.Upstreams { diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index a4a976f7ef7..61d9a4061a4 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -27,6 +27,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/caddy/v2/modules/caddytls" "github.com/caddyserver/certmagic" "github.com/lucas-clemente/quic-go/http3" @@ -134,9 +135,11 @@ type Server struct { listenerWrappers []caddy.ListenerWrapper tlsApp *caddytls.TLS + events *caddyevents.App logger *zap.Logger accessLogger *zap.Logger errorLogger *zap.Logger + ctx caddy.Context h3server *http3.Server } diff --git a/modules/caddytls/event.go b/modules/caddytls/event.go deleted file mode 100644 index 7503cd25bcc..00000000000 --- a/modules/caddytls/event.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddytls - -import ( - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyevent" - "github.com/caddyserver/certmagic" -) - -// onEvent translates certmagic events into caddy events -// then dispatches them asynchronously. -func (t *TLS) onEvent(event string, data interface{}) { - switch event { - // case "cached_managed_cert", "cached_unmanaged_cert": - // subjectNames, ok := data.([]string) - // if !ok { - // return - // } - // t.event.AsyncDispatch(caddyevent.NewGeneric( - // caddy.ModuleID("tls.event."+event), - // caddyevent.DataMap{"subjectNames": subjectNames}, - // )) - - // case "tls_handshake_started", "tls_handshake_completed": - // clientHello, ok := data.(*tls.ClientHelloInfo) - // if !ok { - // return - // } - // t.event.AsyncDispatch(NewHandshakeEvent(event, clientHello)) - - case "cert_obtained", "cert_renewed", "cert_revoked": - eventData, ok := data.(certmagic.CertificateEventData) - if !ok { - return - } - t.event.AsyncDispatch(NewCertEvent(event, eventData)) - } -} - -// CertEvent is dispatched when a certificate is obtained, renewed, or revoked. -type CertEvent struct { - caddyevent.GenericEvent -} - -func NewCertEvent(event string, data certmagic.CertificateEventData) *CertEvent { - certEvent := new(CertEvent) - certEvent.SetID(caddy.ModuleID("tls.event." + event)) - certEvent.Set("name", data.Name) - certEvent.Set("issuerKey", data.IssuerKey) - certEvent.Set("storageKey", data.StorageKey) - return certEvent -} - -func (e CertEvent) GetName() string { - return e.Get("name").(string) -} - -func (e CertEvent) GetIssuerKey() string { - return e.Get("issuerKey").(string) -} - -func (e CertEvent) GetStorageKey() string { - return e.Get("storageKey").(string) -} - -// HandshakeEvent is dispatched when a TLS handshake is started, or is completed. -// type HandshakeEvent struct { -// caddyevent.GenericEvent -// } - -// func NewHandshakeEvent(event string, clientHello *tls.ClientHelloInfo) *HandshakeEvent { -// handshakeEvent := new(HandshakeEvent) -// handshakeEvent.SetID(caddy.ModuleID("tls.event." + event)) -// handshakeEvent.Set("clientHello", clientHello) -// return handshakeEvent -// } - -// func (e CertEvent) GetClientHello() *tls.ClientHelloInfo { -// return e.Get("clientHello").(*tls.ClientHelloInfo) -// } diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index a310d194c5d..b66a9857746 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -25,7 +25,7 @@ import ( "time" "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/modules/caddyevent" + "github.com/caddyserver/caddy/v2/modules/caddyevents" "github.com/caddyserver/certmagic" "go.uber.org/zap" ) @@ -74,7 +74,7 @@ type TLS struct { storageCleanTicker *time.Ticker storageCleanStop chan struct{} logger *zap.Logger - event *caddyevent.EventApp + events *caddyevents.App } // CaddyModule returns the Caddy module information. @@ -87,16 +87,15 @@ func (TLS) CaddyModule() caddy.ModuleInfo { // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { + eventsAppIface, err := ctx.App("events") + if err != nil { + return fmt.Errorf("getting events app: %v", err) + } + t.events = eventsAppIface.(*caddyevents.App) t.ctx = ctx t.logger = ctx.Logger(t) repl := caddy.NewReplacer() - eventAppIface, err := ctx.App("event") - if err != nil { - return fmt.Errorf("getting event app: %v", err) - } - t.event = eventAppIface.(*caddyevent.EventApp) - // set up a new certificate cache; this (re)loads all certificates cacheOpts := certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { @@ -523,6 +522,23 @@ func (t *TLS) storageCleanInterval() time.Duration { return defaultStorageCleanInterval } +// onEvent translates CertMagic events into Caddy events then dispatches them. +// TODO: enhance CertMagic's event features to better accommodate our needs +func (t *TLS) onEvent(eventName string, data interface{}) { + evtData := make(map[string]interface{}) + switch d := data.(type) { + case certmagic.CertificateEventData: + evtData["name"] = d.Name + evtData["issuer_key"] = d.IssuerKey + evtData["storage_key"] = d.StorageKey + case *tls.ClientHelloInfo: + evtData["client_hello"] = d + case []string: + evtData["subject_names"] = d + } + t.events.Emit(t.ctx, eventName, evtData) +} + // CertificateLoader is a type that can load certificates. // Certificates can optionally be associated with tags. type CertificateLoader interface { diff --git a/modules/standard/imports.go b/modules/standard/imports.go index 010742b597a..a9d0b396825 100644 --- a/modules/standard/imports.go +++ b/modules/standard/imports.go @@ -3,7 +3,8 @@ package standard import ( // standard Caddy modules _ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - _ "github.com/caddyserver/caddy/v2/modules/caddyevent" + _ "github.com/caddyserver/caddy/v2/modules/caddyevents" + _ "github.com/caddyserver/caddy/v2/modules/caddyevents/eventsconfig" _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/standard" _ "github.com/caddyserver/caddy/v2/modules/caddypki" _ "github.com/caddyserver/caddy/v2/modules/caddypki/acmeserver" From ed288cade6c4be5bac593d3fb3deed8106687391 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Thu, 25 Aug 2022 16:41:27 -0600 Subject: [PATCH 09/14] gofmt --- context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/context.go b/context.go index 2b46faa481d..e850b73df79 100644 --- a/context.go +++ b/context.go @@ -311,7 +311,7 @@ func (ctx Context) LoadModuleByID(id string, rawMsg json.RawMessage) (any, error if modInfo.New == nil { return nil, fmt.Errorf("module '%s' has no constructor", modInfo.ID) } - + val := modInfo.New() // value must be a pointer for unmarshaling into concrete type, even if From e3cc381bb77b90a421f97aab853c69e920b024ee Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Fri, 26 Aug 2022 12:26:30 -0600 Subject: [PATCH 10/14] Update CertMagic events; finish changing to any --- go.mod | 2 +- go.sum | 4 ++-- modules/caddyevents/app.go | 8 ++++---- modules/caddyevents/eventsconfig/caddyfile.go | 2 +- .../caddyhttp/reverseproxy/healthchecks.go | 4 ++-- modules/caddytls/tls.go | 18 ++++------------- usagepool.go | 20 +++++++++---------- 7 files changed, 24 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 3ad781901dd..54afe164325 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/alecthomas/chroma v0.10.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.16.3 + github.com/caddyserver/certmagic v0.16.4-0.20220826181710-bb468ce4b49f github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/go-chi/chi v4.1.2+incompatible github.com/google/cel-go v0.12.4 diff --git a/go.sum b/go.sum index a80500687fc..b8937aae1e3 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/caddyserver/certmagic v0.16.3 h1:1ZbiU7y5X0MnDjBTXywUbPMs/ScHbgCeeCy/LPh4IZk= -github.com/caddyserver/certmagic v0.16.3/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= +github.com/caddyserver/certmagic v0.16.4-0.20220826181710-bb468ce4b49f h1:QnBKxBLfKC8a15ymcZIfQZ5/wKftu1PQSgQJw09QiMY= +github.com/caddyserver/certmagic v0.16.4-0.20220826181710-bb468ce4b49f/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index 761ea221894..9e55d7e992e 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -124,7 +124,7 @@ func (app *App) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading event subscriber modules: %v", err) } - for _, h := range handlersIface.([]interface{}) { + for _, h := range handlersIface.([]any) { sub.Handlers = append(sub.Handlers, h.(Handler)) } if len(sub.Handlers) == 0 { @@ -197,7 +197,7 @@ func (app *App) On(eventName string, handler Handler) error { // Emit creates and dispatches an event named eventName to all relevant handlers with // the metadata data. Events are emitted and propagated synchronously. The returned Event // value will have any additional information from the invoked handlers. -func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]interface{}) Event { +func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event { id, err := uuid.NewRandom() if err != nil { app.logger.Error("failed generating new event ID", zap.Error(err)) @@ -225,7 +225,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]interf repl.Set("event.name", e.name) repl.Set("event.module", e.origin.CaddyModule().ID) repl.Set("event.data", e.data) - repl.Map(func(key string) (interface{}, bool) { + repl.Map(func(key string) (any, bool) { switch key { case "event.time": return e.ts, true @@ -301,7 +301,7 @@ type Event struct { ts time.Time name string origin caddy.Module - data map[string]interface{} + data map[string]any // If non-nil, the event has been aborted, meaning // propagation has stopped to other handlers and diff --git a/modules/caddyevents/eventsconfig/caddyfile.go b/modules/caddyevents/eventsconfig/caddyfile.go index 6b1347d6cba..9c3fae78cc3 100644 --- a/modules/caddyevents/eventsconfig/caddyfile.go +++ b/modules/caddyevents/eventsconfig/caddyfile.go @@ -39,7 +39,7 @@ func init() { // } // // If is *, then it will bind to all events. -func parseApp(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) { +func parseApp(d *caddyfile.Dispenser, _ any) (any, error) { app := new(caddyevents.App) // consume the option name diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go index 45431bd4f96..cf22d2615bc 100644 --- a/modules/caddyhttp/reverseproxy/healthchecks.go +++ b/modules/caddyhttp/reverseproxy/healthchecks.go @@ -287,7 +287,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre markUnhealthy := func() { // dispatch an event that the host newly became unhealthy if upstream.setHealthy(false) { - h.events.Emit(h.ctx, "unhealthy", map[string]interface{}{"host": hostAddr}) + h.events.Emit(h.ctx, "unhealthy", map[string]any{"host": hostAddr}) } } @@ -353,7 +353,7 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, upstre // passed health check parameters, so mark as healthy if upstream.setHealthy(true) { h.HealthChecks.Active.logger.Info("host is up", zap.String("host", hostAddr)) - h.events.Emit(h.ctx, "healthy", map[string]interface{}{"host": hostAddr}) + h.events.Emit(h.ctx, "healthy", map[string]any{"host": hostAddr}) } return nil diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 0cc349eb104..fc5f2acee29 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -15,6 +15,7 @@ package caddytls import ( + "context" "crypto/tls" "encoding/json" "fmt" @@ -523,20 +524,9 @@ func (t *TLS) storageCleanInterval() time.Duration { } // onEvent translates CertMagic events into Caddy events then dispatches them. -// TODO: enhance CertMagic's event features to better accommodate our needs -func (t *TLS) onEvent(eventName string, data interface{}) { - evtData := make(map[string]interface{}) - switch d := data.(type) { - case certmagic.CertificateEventData: - evtData["name"] = d.Name - evtData["issuer_key"] = d.IssuerKey - evtData["storage_key"] = d.StorageKey - case *tls.ClientHelloInfo: - evtData["client_hello"] = d - case []string: - evtData["subject_names"] = d - } - t.events.Emit(t.ctx, eventName, evtData) +func (t *TLS) onEvent(ctx context.Context, eventName string, data map[string]any) error { + evt := t.events.Emit(t.ctx, eventName, data) + return evt.Aborted } // CertificateLoader is a type that can load certificates. diff --git a/usagepool.go b/usagepool.go index c34441554a4..7007849fb95 100644 --- a/usagepool.go +++ b/usagepool.go @@ -25,15 +25,15 @@ import ( // only inserted if they do not already exist. There // are two ways to add values to the pool: // -// 1) LoadOrStore will increment usage and store the -// value immediately if it does not already exist. -// 2) LoadOrNew will atomically check for existence -// and construct the value immediately if it does -// not already exist, or increment the usage -// otherwise, then store that value in the pool. -// When the constructed value is finally deleted -// from the pool (when its usage reaches 0), it -// will be cleaned up by calling Destruct(). +// 1. LoadOrStore will increment usage and store the +// value immediately if it does not already exist. +// 2. LoadOrNew will atomically check for existence +// and construct the value immediately if it does +// not already exist, or increment the usage +// otherwise, then store that value in the pool. +// When the constructed value is finally deleted +// from the pool (when its usage reaches 0), it +// will be cleaned up by calling Destruct(). // // The use of LoadOrNew allows values to be created // and reused and finally cleaned up only once, even @@ -196,7 +196,7 @@ func (up *UsagePool) Delete(key any) (deleted bool, err error) { // References returns the number of references (count of usages) to a // key in the pool, and true if the key exists, or false otherwise. -func (up *UsagePool) References(key interface{}) (int, bool) { +func (up *UsagePool) References(key any) (int, bool) { up.RLock() upv, loaded := up.pool[key] up.RUnlock() From fe5882303d6b499b5f7aed67206821215be56e38 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Mon, 29 Aug 2022 12:28:51 -0600 Subject: [PATCH 11/14] Mark as experimental --- modules/caddyevents/app.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index 9e55d7e992e..d6258c45001 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -67,6 +67,9 @@ func init() { // event handlers are invoked in an arbitrary order. // Event handlers should not rely on the logic of other // handlers to succeed. +// +// The entirety of this app module is EXPERIMENTAL and +// subject to change. Pay attention to release notes. type App struct { // Subscriptions bind handlers to one or more events // either globally or scoped to specific modules or module From 1ee09e0d1d17f475f9cfc261166259b4f4bfdd39 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 31 Aug 2022 12:47:32 -0600 Subject: [PATCH 12/14] Remove exec event handler This will be a separate plugin for now --- modules/caddyevents/app.go | 44 +++++++++++-------- modules/caddyevents/exec.go | 88 ------------------------------------- 2 files changed, 25 insertions(+), 107 deletions(-) delete mode 100644 modules/caddyevents/exec.go diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index d6258c45001..d743c9edc0b 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -91,6 +91,7 @@ type Subscription struct { // The ID or namespace of the module(s) from which events // originate to listen to for events. Default: all modules. + // // Events propagate up, so events emitted by module "a.b.c" // will also trigger the event for "a.b" and "a". Thus, to // receive all events from "a.b.c" and "a.b.d", for example, @@ -203,12 +204,13 @@ func (app *App) On(eventName string, handler Handler) error { func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) Event { id, err := uuid.NewRandom() if err != nil { - app.logger.Error("failed generating new event ID", zap.Error(err)) + app.logger.Error("failed generating new event ID", + zap.Error(err), + zap.String("event", eventName)) } eventName = strings.ToLower(eventName) - // TODO: make pointer? e := Event{ id: id, ts: time.Now(), @@ -223,17 +225,22 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E repl = caddy.NewReplacer() ctx.Context = context.WithValue(ctx.Context, caddy.ReplacerCtxKey, repl) } - repl.Set("event", e) - repl.Set("event.id", e.id) - repl.Set("event.name", e.name) - repl.Set("event.module", e.origin.CaddyModule().ID) - repl.Set("event.data", e.data) repl.Map(func(key string) (any, bool) { switch key { + case "event": + return e, true + case "event.id": + return e.id, true + case "event.name": + return e.name, true case "event.time": return e.ts, true case "event.time_unix": return e.ts.UnixMilli(), true + case "event.module": + return e.origin.CaddyModule().ID, true + case "event.data": + return e.data, true } if strings.HasPrefix(key, "event.data.") { @@ -265,15 +272,17 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E } for _, handler := range app.subscriptions[eventName][moduleID] { - // TODO: maybe log handler invocations instead of all event emissions? - err := handler.Handle(ctx, e) - if errors.Is(err, ErrAborted) { - // TODO: Figure out proper way to implement this - e.Aborted = err - return e - } - if err != nil { - app.logger.Error("handler error", zap.Error(err)) + if handler.Handle(ctx, e); err != nil { + aborted := errors.Is(err, ErrAborted) + + app.logger.Error("handler error", + zap.Error(err), + zap.Bool("aborted", aborted)) + + if aborted { + e.Aborted = err + return e + } } } @@ -351,9 +360,6 @@ type Handler interface { Handle(context.Context, Event) error } -// // HandlerFunc -// type HandlerFunc func(context.Context, Event) error - // Interface guards var ( _ caddy.App = (*App)(nil) diff --git a/modules/caddyevents/exec.go b/modules/caddyevents/exec.go deleted file mode 100644 index d5962e73d5b..00000000000 --- a/modules/caddyevents/exec.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2015 Matthew Holt and The Caddy Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package caddyevents - -import ( - "context" - "os" - "os/exec" - - "github.com/caddyserver/caddy/v2" - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "go.uber.org/zap" -) - -func init() { - caddy.RegisterModule(ExecHandler{}) -} - -// ExecHandler implements an event handler that runs a command/program. -type ExecHandler struct { - Command string `json:"command,omitempty"` - Args []string `json:"args,omitempty"` - Dir string `json:"dir,omitempty"` - Timeout caddy.Duration `json:"timeout,omitempty"` - - logger *zap.Logger -} - -// CaddyModule returns the Caddy module information. -func (ExecHandler) CaddyModule() caddy.ModuleInfo { - return caddy.ModuleInfo{ - ID: "events.handlers.exec", - New: func() caddy.Module { return new(ExecHandler) }, - } -} - -// Provision sets up the module. -func (eh *ExecHandler) Provision(ctx caddy.Context) error { - eh.logger = ctx.Logger(eh) - return nil -} - -func (eh *ExecHandler) Handle(ctx context.Context, e Event) error { - repl := ctx.Value(caddy.ReplacerCtxKey).(*caddy.Replacer) - - // expand placeholders in command args; - // notably, WE DO NOT EXPAND PLACEHOLDERS - // IN THE COMMAND ITSELF for safety reasons - expandedArgs := make([]string, len(eh.Args)) - for i := range eh.Args { - expandedArgs[i] = repl.ReplaceAll(eh.Args[i], "") - } - - cmd := exec.Command(eh.Command, expandedArgs...) - cmd.Dir = eh.Dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} - -func (eh *ExecHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { - for d.Next() { - if !d.NextArg() { - return d.ArgErr() - } - eh.Command = d.Val() - eh.Args = d.RemainingArgs() - } - return nil -} - -// Interface guards -var ( - _ caddyfile.Unmarshaler = (*ExecHandler)(nil) - _ caddy.Provisioner = (*ExecHandler)(nil) -) From ab34b55d239eb31d889c7ee72edc002849c6a732 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 31 Aug 2022 13:36:36 -0600 Subject: [PATCH 13/14] Oops, read returned error value --- modules/caddyevents/app.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/caddyevents/app.go b/modules/caddyevents/app.go index d743c9edc0b..0c05fe58811 100644 --- a/modules/caddyevents/app.go +++ b/modules/caddyevents/app.go @@ -41,8 +41,8 @@ func init() { // control program flow in some cases. // // Events are propagated in a DOM-like fashion. An event -// event emitted from module `a.b.c` (the "origin") will -// first invoke handlers listening to `a.b.c`, then `a.b`, +// emitted from module `a.b.c` (the "origin") will first +// invoke handlers listening to `a.b.c`, then `a.b`, // then `a`, then those listening regardless of origin. // If a handler returns the special error Aborted, then // propagation immediately stops and the event is marked @@ -272,7 +272,7 @@ func (app *App) Emit(ctx caddy.Context, eventName string, data map[string]any) E } for _, handler := range app.subscriptions[eventName][moduleID] { - if handler.Handle(ctx, e); err != nil { + if err := handler.Handle(ctx, e); err != nil { aborted := errors.Is(err, ErrAborted) app.logger.Error("handler error", From f56f88cec7003f4b91e32472b46291afb8c3201a Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 31 Aug 2022 14:52:40 -0600 Subject: [PATCH 14/14] Use latest CertMagic (v0.17.0) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b7174cfc870..8504d9cc939 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/alecthomas/chroma v0.10.0 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b - github.com/caddyserver/certmagic v0.16.4-0.20220826181710-bb468ce4b49f + github.com/caddyserver/certmagic v0.17.0 github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac github.com/go-chi/chi v4.1.2+incompatible github.com/google/cel-go v0.12.4 diff --git a/go.sum b/go.sum index 996c840a677..88e53ac9f51 100644 --- a/go.sum +++ b/go.sum @@ -131,8 +131,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= -github.com/caddyserver/certmagic v0.16.4-0.20220826181710-bb468ce4b49f h1:QnBKxBLfKC8a15ymcZIfQZ5/wKftu1PQSgQJw09QiMY= -github.com/caddyserver/certmagic v0.16.4-0.20220826181710-bb468ce4b49f/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= +github.com/caddyserver/certmagic v0.17.0 h1:AHHvvmv6SNcq0vK5BgCevQqYMV8GNprVk6FWZzx8d+Q= +github.com/caddyserver/certmagic v0.17.0/go.mod h1:pSS2aZcdKlrTZrb2DKuRafckx20o5Fz1EdDKEB8KOQM= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=