-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhandler.go
More file actions
193 lines (157 loc) · 4.99 KB
/
Copy pathhandler.go
File metadata and controls
193 lines (157 loc) · 4.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package certwebhook
import (
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
"go.uber.org/zap"
)
const (
LogMsgConfigValidationFailed = "configuration validation failed"
LogMsgFailedToSubscribeToEvents = "failed to subscribe to events"
LogMsgFailedToCreatePortalClient = "failed to create portal client"
LogMsgStarting = "cert_webhook app starting"
LogMsgStarted = "cert_webhook app started"
LogMsgStopping = "cert_webhook app stopping"
LogMsgStopped = "cert_webhook app stopped"
LogMsgWebhookDeliveryNotInitialized = "webhook delivery not initialized"
LogMsgWebhookThrottled = "webhook throttled for domain"
LogMsgSkippingIPAddress = "skipping webhook for IP address"
LogMsgSkippingIgnoredDomain = "skipping webhook for ignored domain"
)
const defaultThrottleInterval = 5 * time.Minute
type lastSentEntry struct {
status SSLStatus
time time.Time
}
type throttleMap struct {
mu sync.Mutex
lastSent map[string]lastSentEntry
interval time.Duration
}
type CertWebhookApp struct {
Config `json:"-"`
logger *zap.Logger
ctx caddy.Context
portal *PortalClient
delivery *WebhookDelivery
eventsApp *caddyevents.App
throttle *throttleMap
throttleInterval time.Duration
certStatusFn certStatusFunc
ignoredDomains map[string]struct{}
}
func (CertWebhookApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "cert_webhook",
New: func() caddy.Module { return new(CertWebhookApp) },
}
}
func (a *CertWebhookApp) Provision(ctx caddy.Context) error {
a.logger = ctx.Logger(a)
a.ctx = ctx
a.Config.Provision()
a.throttleInterval = a.Config.throttleInterval()
a.throttle = &throttleMap{
lastSent: make(map[string]lastSentEntry),
interval: a.throttleInterval,
}
a.certStatusFn = defaultCertStatusFn
a.ignoredDomains = make(map[string]struct{}, len(a.IgnoredDomains))
for _, d := range a.IgnoredDomains {
a.ignoredDomains[strings.ToLower(d)] = struct{}{}
}
initMetrics(a.ctx.GetMetricsRegistry())
initTracer()
a.logger.Debug("config resolved",
zap.String("portal_url", a.PortalURL),
zap.Bool("gateway_secret_set", a.GatewaySecret != ""),
zap.Duration("throttle_interval", a.throttleInterval),
zap.Strings("ignored_domains", a.IgnoredDomains))
if err := a.Config.Validate(); err != nil {
return err
}
// Subscribe to events during provisioning, before the events app starts.
// Caddy's event bus rejects new subscriptions after Start() is called.
if err := a.subscribeToEvents(ctx); err != nil {
a.logger.Error(LogMsgFailedToSubscribeToEvents, zap.Error(err))
return err
}
return nil
}
func (a *CertWebhookApp) Start() error {
a.logger.Info(LogMsgStarting)
portal, err := NewPortalClient(a.PortalURL, a.GatewaySecret)
if err != nil {
a.logger.Error(LogMsgFailedToCreatePortalClient, zap.Error(err))
return err
}
a.portal = portal
a.delivery = NewWebhookDelivery(a.portal.Websites(), a.logger)
a.logger.Info(LogMsgStarted,
zap.String("portal_url", a.PortalURL))
return nil
}
func (a *CertWebhookApp) Stop() error {
a.logger.Info(LogMsgStopping)
if a.delivery != nil {
a.delivery.Wait()
}
if a.portal != nil {
a.portal.Close()
}
a.portal = nil
a.delivery = nil
a.eventsApp = nil
a.throttle = nil
a.logger.Info(LogMsgStopped)
return nil
}
func (a *CertWebhookApp) sendWebhook(domain string, status SSLStatus, errorMsg, timestamp string) error {
if isIPAddress(domain) {
a.logger.Debug(LogMsgSkippingIPAddress,
zap.String("domain", domain))
return nil
}
if _, ok := a.ignoredDomains[strings.ToLower(domain)]; ok {
a.logger.Debug(LogMsgSkippingIgnoredDomain,
zap.String("domain", domain))
return nil
}
if a.delivery == nil {
a.logger.Error(LogMsgWebhookDeliveryNotInitialized)
return fmt.Errorf("webhook delivery not initialized")
}
a.logger.Debug("sending webhook",
zap.String("domain", domain),
zap.String("status", string(status)),
zap.String("timestamp", timestamp))
a.delivery.deliverAsync(domain, status, errorMsg, timestamp)
return nil
}
// isIPAddress returns true for raw IP addresses. Caddy issues certs
// for the gateway's listen address (e.g. 104.243.38.32) and the cert_webhook
// would otherwise fire a webhook to the portal, which 404s because there's no
// website record for an IP.
func isIPAddress(domain string) bool {
return net.ParseIP(domain) != nil
}
func (a *CertWebhookApp) shouldSend(domain string, status SSLStatus) bool {
if a.throttle == nil {
return true
}
return a.throttle.checkAndMark(domain, status)
}
func (tm *throttleMap) checkAndMark(domain string, status SSLStatus) bool {
tm.mu.Lock()
defer tm.mu.Unlock()
last, ok := tm.lastSent[domain]
if !ok || last.status != status || time.Since(last.time) >= tm.interval {
tm.lastSent[domain] = lastSentEntry{status: status, time: time.Now()}
return true
}
return false
}