@@ -11,54 +11,44 @@ import (
1111 "go.uber.org/zap"
1212)
1313
14- // Caddy event names for certificate lifecycle
1514const (
16- // EventCertObtained is fired when a certificate is first obtained
1715 EventCertObtained = "cert_obtained"
1816
19- // EventCertRenewed is fired when a certificate is renewed
2017 EventCertRenewed = "cert_renewed"
2118
22- // EventCertExpired is fired when a certificate expires
2319 EventCertExpired = "cert_expired"
20+
21+ EventTLSGetCertificate = "tls_get_certificate"
2422)
2523
26- // Log message constants
2724const (
2825 LogMsgEventsAppNotAvailable = "events app not available"
2926 LogMsgEventsAppNotExpectedType = "events app is not of expected type"
3027 LogMsgSubscribedToCertObtained = "subscribed to cert_obtained event"
3128 LogMsgSubscribedToCertRenewed = "subscribed to cert_renewed event"
3229 LogMsgSubscribedToCertExpired = "subscribed to cert_expired event"
30+ LogMsgSubscribedToTLSGetCert = "subscribed to tls_get_certificate event"
3331 LogMsgUnknownEventType = "unknown event type"
3432 LogMsgFailedToExtractEventData = "failed to extract event data"
3533 LogMsgFailedToMapEventToStatus = "failed to map event to status"
3634 LogMsgCertificateEventProcessed = "certificate event processed"
35+ LogMsgTLSGetCertThrottled = "tls_get_certificate event throttled"
36+ LogMsgTLSGetCertProcessed = "tls_get_certificate event processed"
3737)
3838
39- // EventData represents certificate event data from Caddy
4039type EventData struct {
41- // Domain is the certificate domain name
4240 Domain string `json:"domain"`
4341
44- // Timestamp is when the event occurred (ISO 8601 format)
4542 Timestamp string `json:"timestamp"`
4643
47- // Error is any error that occurred during certificate operation
4844 Error string `json:"error,omitempty"`
4945
50- // Raw is the raw event data for debugging
5146 Raw map [string ]any `json:"-"`
5247
53- // EventType is the type of event (cert_obtained, cert_renewed, cert_expired)
5448 EventType string `json:"event_type"`
5549}
5650
57- // Event handlers for certificate lifecycle events
58-
59- // subscribeToEvents registers handlers for certificate events
6051func (h * CertWebhookApp ) subscribeToEvents (ctx caddy.Context ) error {
61- // Get the events app from context
6252 eventsAppIface , err := ctx .App ("events" )
6353 if err != nil {
6454 h .logger .Warn (LogMsgEventsAppNotAvailable , zap .Error (err ))
@@ -71,10 +61,8 @@ func (h *CertWebhookApp) subscribeToEvents(ctx caddy.Context) error {
7161 return nil
7262 }
7363
74- // Store reference for cleanup
7564 h .eventsApp = eventsApp
7665
77- // Subscribe to cert_obtained event
7866 if err := eventsApp .On (EventCertObtained , h ); err != nil {
7967 return fmt .Errorf ("failed to subscribe to cert_obtained event: %w" , err )
8068 }
@@ -90,23 +78,28 @@ func (h *CertWebhookApp) subscribeToEvents(ctx caddy.Context) error {
9078 }
9179 h .logger .Info (LogMsgSubscribedToCertExpired )
9280
81+ if err := eventsApp .On (EventTLSGetCertificate , h ); err != nil {
82+ return fmt .Errorf ("failed to subscribe to tls_get_certificate event: %w" , err )
83+ }
84+ h .logger .Info (LogMsgSubscribedToTLSGetCert )
85+
9386 return nil
9487}
9588
96- // Handle implements caddyevents.Handler interface to process certificate events
9789func (h * CertWebhookApp ) Handle (ctx context.Context , data caddy.Event ) error {
9890 eventName := data .Name ()
9991
10092 switch eventName {
10193 case EventCertObtained , EventCertRenewed , EventCertExpired :
10294 return h .handleCertEvent (eventName , data )
95+ case EventTLSGetCertificate :
96+ return h .handleTLSGetCertificateEvent (data )
10397 default :
10498 h .logger .Warn (LogMsgUnknownEventType , zap .String ("event" , eventName ))
10599 return nil
106100 }
107101}
108102
109- // handleCertEvent processes certificate lifecycle events (obtained, renewed, expired)
110103func (h * CertWebhookApp ) handleCertEvent (eventType string , data caddy.Event ) error {
111104 h .logger .Debug ("handling cert event" , zap .String ("event_type" , eventType ))
112105
@@ -126,6 +119,14 @@ func (h *CertWebhookApp) handleCertEvent(eventType string, data caddy.Event) err
126119 return err
127120 }
128121
122+ if ! h .shouldSend (eventData .Domain , status ) {
123+ h .logger .Debug ("cert event throttled" ,
124+ zap .String ("event_type" , eventType ),
125+ zap .String ("domain" , eventData .Domain ),
126+ zap .String ("status" , string (status )))
127+ return nil
128+ }
129+
129130 h .logger .Info (LogMsgCertificateEventProcessed ,
130131 zap .String ("event_type" , eventType ),
131132 zap .String ("domain" , eventData .Domain ),
@@ -135,6 +136,55 @@ func (h *CertWebhookApp) handleCertEvent(eventType string, data caddy.Event) err
135136 return h .sendWebhook (eventData .Domain , status , eventData .Error , eventData .Timestamp )
136137}
137138
139+ func (h * CertWebhookApp ) handleTLSGetCertificateEvent (data caddy.Event ) error {
140+ domain , err := extractDomainFromClientHello (data )
141+ if err != nil {
142+ h .logger .Debug ("failed to extract domain from tls_get_certificate" , zap .Error (err ))
143+ return nil
144+ }
145+
146+ status := h .certStatusFn (domain )
147+
148+ if ! h .shouldSend (domain , status ) {
149+ h .logger .Debug (LogMsgTLSGetCertThrottled ,
150+ zap .String ("domain" , domain ),
151+ zap .String ("status" , string (status )))
152+ return nil
153+ }
154+
155+ timestamp := time .Now ().UTC ().Format (time .RFC3339 )
156+
157+ h .logger .Info (LogMsgTLSGetCertProcessed ,
158+ zap .String ("domain" , domain ),
159+ zap .String ("status" , string (status )),
160+ zap .String ("timestamp" , timestamp ))
161+
162+ return h .sendWebhook (domain , status , "" , timestamp )
163+ }
164+
165+ func extractDomainFromClientHello (event caddy.Event ) (string , error ) {
166+ if len (event .Data ) == 0 {
167+ return "" , fmt .Errorf ("event data is empty" )
168+ }
169+
170+ raw := make (map [string ]any )
171+ if err := decodeJSON (event .Data , & raw ); err != nil {
172+ return "" , fmt .Errorf ("failed to unmarshal event data: %w" , err )
173+ }
174+
175+ clientHello , ok := raw ["client_hello" ].(map [string ]any )
176+ if ! ok {
177+ return "" , fmt .Errorf ("client_hello not found in event data" )
178+ }
179+
180+ serverName , ok := clientHello ["ServerName" ].(string )
181+ if ! ok || serverName == "" {
182+ return "" , fmt .Errorf ("ServerName not found in client_hello" )
183+ }
184+
185+ return serverName , nil
186+ }
187+
138188// extractEventData extracts domain, timestamp, and error information from Caddy event data
139189func (h * CertWebhookApp ) extractEventData (eventType string , event caddy.Event ) (* EventData , error ) {
140190 data := & EventData {
0 commit comments