Skip to content

Commit

Permalink
Merge pull request #60 from nwesterhausen/fix-scheduler
Browse files Browse the repository at this point in the history
Fix scheduler
  • Loading branch information
nwesterhausen authored Apr 9, 2024
2 parents 1f6be79 + bc3df7a commit 63ff81c
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 20 deletions.
103 changes: 101 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,24 @@ import (
)

func main() {

// setup the data directory which is passed in via a program argument
dataDirectory := flag.String("data-dir", "./data", "Directory to store configuration and cache files")
flag.Parse()

// output the data directory to log and validate it
log.Println("📁 Data directory set to", *dataDirectory)
validateDirectory(*dataDirectory)

// setup the configuration directory
configDirectory := configuration.ConfigDirectory{DataDir: *dataDirectory}

log.Println("⤴️ Loading configuration and cache files...")

// read the app configuration
config := configDirectory.ReadAppConfig()
// configure the SMTP mailer
var _mailer *service.MailerService = nil
// provide some sanity log messages, to confirm the alert and mailer settings
if config.Config.Alerts.SendAlerts {
if !config.Config.SMTP.Enabled {
log.Println("❌ Email notifications are disabled")
Expand All @@ -42,24 +47,34 @@ func main() {
} else {
log.Println("📵 Alerts are disabled")
}
// for sanity, log the cache refresh interval parsed from the configuration
log.Printf("📆 WHOIS cache refresh interval set to %s", configuration.WhoisRefreshInterval)

// read the domain configuration
domains := configDirectory.ReadDomains()
log.Printf("📄 Loaded %d domains from domain list", len(domains.DomainFile.Domains))

// read the WHOIS cache
whoisCache := configDirectory.ReadWhoisCache()
log.Printf("📄 Found %d cached whois entries", len(whoisCache.FileContents.Entries))

// initialize the web server
app := echo.New()

// tell it we will provide custom error pages
app.HTTPErrorHandler = handlers.CustomHTTPErrorHandler

// tell it about the static asset directories
app.Static("/", "assets")
// use the logger middleware
app.Use(middleware.Logger())

// set up our routes
handlers.SetupRoutes(app)
handlers.SetupConfigRoutes(app, config)
handlers.SetupDomainRoutes(app, domains)

// if the mailer was configured, add the routes
if _mailer != nil {
handlers.SetupMailerRoutes(app, _mailer, config.Config.Alerts.Admin)
}
Expand All @@ -77,18 +92,102 @@ func main() {
}

whoisRefreshOnSchedule(whoisCache, domains, configuration.WhoisRefreshInterval)
log.Printf("📆 Refreshing WHOIS cache every %s", configuration.WhoisRefreshInterval)
log.Printf("📆 Scheduler running WHOIS expiration checks every %s", configuration.WhoisRefreshInterval)
})

// Connect scheduler for domain expiration checks. First delay is after 60 seconds, then every (configured amount) of hours
// Does not automatically update the interval if the config changes, so a server reset is required to change the interval
// This uses the WhoisRefreshInterval as the interval for the domain expiration checks
time.AfterFunc(60*time.Second, func() {
domainExpirationCheckOnSchedule(whoisCache, domains, _mailer, config.Config, configuration.WhoisRefreshInterval)
log.Printf("📆 Scheduler running domain expiration checks every %s", configuration.WhoisRefreshInterval)
})

// Start server on configured port
app.Logger.Fatal(app.Start(":" + fmt.Sprint(config.Config.App.Port)))
}

// When called on schedule, check for domain expirations in the WHOIS cache and send mail
func domainExpirationCheckOnSchedule(whoisCache configuration.WhoisCacheStorage, domains configuration.DomainConfiguration, mailer *service.MailerService, appConfig configuration.ConfigurationFile, interval time.Duration) {
if mailer == nil {
log.Println("🚫 No mailer configured, canceling domain expiration checks.")
return
}

// for every domain in the domains configuration, if alerts are turned on, check the expiration from the WHOIS cache and then send an alert if one hasn't been sent.
for _, domain := range domains.DomainFile.Domains {
if domain.Alerts {
whoisEntry := whoisCache.Get(domain.FQDN)
if whoisEntry == nil {
log.Printf("❌ WHOIS entry for %s not found, skipping", domain.FQDN)
continue
}

// Get the days until expiration
daysUntilExpiration := whoisEntry.WhoisInfo.Domain.ExpirationDateInTime.Sub(time.Now()).Hours() / 24

// Check the 2-month, 1-month, 2-week, 1-week, 3-day and within 1 week of expiration thresholds
if daysUntilExpiration <= 60 && !whoisEntry.Sent2MonthAlert && appConfig.Alerts.Send2MonthAlert {
if err := mailer.SendAlert(appConfig.Alerts.Admin, domain.FQDN, configuration.Alert2Months); err != nil {
log.Printf("❌ Failed to send 2-month alert for %s: %s", domain.FQDN, err)
continue
}
whoisEntry.MarkAlertSent(configuration.Alert2Months)
}
if daysUntilExpiration <= 30 && !whoisEntry.Sent1MonthAlert && appConfig.Alerts.Send1MonthAlert {
if err := mailer.SendAlert(appConfig.Alerts.Admin, domain.FQDN, configuration.Alert1Month); err != nil {
log.Printf("❌ Failed to send 1-month alert for %s: %s", domain.FQDN, err)
continue
}
whoisEntry.MarkAlertSent(configuration.Alert1Month)
}
if daysUntilExpiration <= 14 && !whoisEntry.Sent2WeekAlert && appConfig.Alerts.Send2WeekAlert {
if err := mailer.SendAlert(appConfig.Alerts.Admin, domain.FQDN, configuration.Alert2Weeks); err != nil {
log.Printf("❌ Failed to send 2-week alert for %s: %s", domain.FQDN, err)
continue
}
whoisEntry.MarkAlertSent(configuration.Alert2Weeks)
}
if daysUntilExpiration <= 7 && !whoisEntry.Sent1WeekAlert && appConfig.Alerts.Send1WeekAlert {
if err := mailer.SendAlert(appConfig.Alerts.Admin, domain.FQDN, configuration.Alert1Week); err != nil {
log.Printf("❌ Failed to send 1-week alert for %s: %s", domain.FQDN, err)
continue
}
whoisEntry.MarkAlertSent(configuration.Alert1Week)
}
if daysUntilExpiration <= 3 && !whoisEntry.Sent3DayAlert && appConfig.Alerts.Send3DayAlert {
if err := mailer.SendAlert(appConfig.Alerts.Admin, domain.FQDN, configuration.Alert3Days); err != nil {
log.Printf("❌ Failed to send 3-day alert for %s: %s", domain.FQDN, err)
continue
}
whoisEntry.MarkAlertSent(configuration.Alert3Days)
}
// The daily alerts within one week of expiration need to check the last alert sent date, and confirm that expiration is within 7 days
if daysUntilExpiration <= 7 && daysUntilExpiration > 0 && appConfig.Alerts.SendDailyExpiryAlert {
// Check if the last alert sent was on this day, month and year. If it was, don't send another alert.
if whoisEntry.LastAlertSent.Day() == time.Now().Day() && whoisEntry.LastAlertSent.Month() == time.Now().Month() && whoisEntry.LastAlertSent.Year() == time.Now().Year() {
log.Printf("⚠️ Daily alert for %s was already sent today", domain.FQDN)
continue
}

if err := mailer.SendAlert(appConfig.Alerts.Admin, domain.FQDN, configuration.AlertDaily); err != nil {
log.Printf("❌ Failed to send daily alert for %s: %s", domain.FQDN, err)
continue
}
whoisEntry.MarkAlertSent(configuration.AlertDaily)
}
}
}

time.AfterFunc(interval, func() { domainExpirationCheckOnSchedule(whoisCache, domains, mailer, appConfig, interval) })
}

// Refresh the whois cache on a schedule, and flush the cache. This runs every 6 hours.
func whoisRefreshOnSchedule(whoisCache configuration.WhoisCacheStorage, domains configuration.DomainConfiguration, interval time.Duration) {
log.Println("🔄 Refreshing WHOIS cache")
whoisCache.RefreshWithDomains(domains)
whoisCache.Flush()

time.AfterFunc(interval, func() { whoisRefreshOnSchedule(whoisCache, domains, interval) })
}

Expand Down
6 changes: 4 additions & 2 deletions configuration/whois-cache.configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ func (w *WhoisCacheStorage) Add(fqdn string) {
LastUpdated: time.Time{},
}

// Perform the whois query
// Perform the whois query for the new domain
newEntry.Refresh()

// Add the entry to the list
w.FileContents.Entries = append(w.FileContents.Entries, newEntry)
// Flush the cache to disk
w.Flush()
}

func (w *WhoisCacheStorage) Refresh() {
Expand Down Expand Up @@ -221,7 +223,7 @@ func (w *WhoisCache) MarkAlertSent(alert Alert) {
case AlertDaily:
// Check if the alert has already been sent, and log the inconsistency
// We have to check if the date stored is today to know if we sent it already
if w.LastAlertSent == time.Now() {
if w.LastAlertSent.Day() == time.Now().Day() && w.LastAlertSent.Month() == time.Now().Month() && w.LastAlertSent.Year() == time.Now().Year() {
log.Printf("⚠️ %s was already marked as sent for %s!", alert, w.FQDN)
}
}
Expand Down
11 changes: 9 additions & 2 deletions handlers/whois.handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package handlers
import (
"errors"

"github.com/a-h/templ"
"github.com/labstack/echo/v4"
"github.com/nwesterhausen/domain-monitor/service"
"github.com/nwesterhausen/domain-monitor/views/domains"
Expand All @@ -25,8 +26,14 @@ func (h *WhoisHandler) GetCard(c echo.Context) error {
return errors.New("invalid domain to fetch (FQDN required)")
}

whois := h.WhoisService.GetWhois(fqdn)
card := domains.WhoisDetail(whois)
var card templ.Component
whois, err := h.WhoisService.GetWhois(fqdn)

if err != nil {
card = domains.WhoisError(err)
} else {
card = domains.WhoisDetail(whois)
}

return View(c, card)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "1.0.0-rc1",
"version": "1.1.0",
"repository": {
"type": "git",
"url": "https://github.com/nwesterhausen/domain-monitor"
Expand Down
8 changes: 4 additions & 4 deletions service/domain.service.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func (s *ServicesDomain) CreateDomain(domain configuration.Domain) (int, error)
}
}
// This should never happen.. but just in case return -1 and an error
return -1, errors.New("Failed to add domain")
return -1, errors.New("failed to add domain")
}

func (s *ServicesDomain) GetDomain(fqdn string) (configuration.Domain, error) {
Expand All @@ -33,7 +33,7 @@ func (s *ServicesDomain) GetDomain(fqdn string) (configuration.Domain, error) {
return d, nil
}
}
return configuration.Domain{}, errors.New("Domain not found")
return configuration.Domain{}, errors.New("domain not found")
}

func (s *ServicesDomain) GetDomains() ([]configuration.Domain, error) {
Expand All @@ -52,7 +52,7 @@ func (s *ServicesDomain) UpdateDomain(domain configuration.Domain) error {
}
}
// This should never happen.. but just in case return an error
return errors.New("Failed to update domain")
return errors.New("failed to update domain")
}

func (s *ServicesDomain) DeleteDomain(fqdn string) error {
Expand All @@ -65,7 +65,7 @@ func (s *ServicesDomain) DeleteDomain(fqdn string) error {
// Return nil to indicate success (we can confirm the domain was deleted by checking the list)
for _, d := range s.store.DomainFile.Domains {
if d.FQDN == fqdn {
return errors.New("Failed to delete domain")
return errors.New("failed to delete domain")
}
}
return nil
Expand Down
27 changes: 27 additions & 0 deletions service/mailer.service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"fmt"
"log"

"github.com/nwesterhausen/domain-monitor/configuration"
Expand Down Expand Up @@ -78,3 +79,29 @@ func (m *MailerService) TestMail(to string) error {

return nil
}

func (m *MailerService) SendAlert(to string, fqdn string, alert configuration.Alert) error {
msg := mail.NewMsg()
if err := msg.From(m.from); err != nil {
log.Printf("❌ failed to set FROM address: %s", err)
return err
}
if err := msg.To(to); err != nil {
log.Printf("❌ failed to set TO address: %s", err)
return err
}
msg.Subject("Domain Expiration Alert: " + fqdn)

body := fmt.Sprintf("Your domain %s is expiring in %s. Please renew it as soon as possible.", fqdn, alert)

msg.SetBodyString(mail.TypeTextPlain, body)

if err := m.client.DialAndSend(msg); err != nil {
log.Printf("❌ failed to deliver mail: %s", err)
return err
}

log.Printf("📧 E-mail message sent to " + to)

return nil
}
18 changes: 14 additions & 4 deletions service/whois.service.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package service

import (
"errors"
"log"

"github.com/nwesterhausen/domain-monitor/configuration"
Expand All @@ -14,15 +15,24 @@ func NewWhoisService(store configuration.WhoisCacheStorage) *ServicesWhois {
return &ServicesWhois{store: store}
}

func (s *ServicesWhois) GetWhois(fqdn string) configuration.WhoisCache {
func (s *ServicesWhois) GetWhois(fqdn string) (configuration.WhoisCache, error) {
for _, entry := range s.store.FileContents.Entries {
if entry.FQDN == fqdn {
return entry
return entry, nil
}
}
log.Println("WHOIS entry cache miss for", fqdn)
log.Println("🙅 WHOIS entry cache miss for", fqdn)

return configuration.WhoisCache{}
// Since we cache missed, let's try to fetch the WHOIS entry instead
s.store.Add(fqdn)
// Try to get the entry again
for _, entry := range s.store.FileContents.Entries {
if entry.FQDN == fqdn {
return entry, nil
}
}

return configuration.WhoisCache{}, errors.New("entry missing")
}

func (s *ServicesWhois) MarkAlertSent(fqdn string, alert configuration.Alert) bool {
Expand Down
10 changes: 5 additions & 5 deletions views/domains/domain-dashboard.templ
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/nwesterhausen/domain-monitor/configuration"
"strings"
"time"
"fmt"

"github.com/hako/durafmt"
)
Expand Down Expand Up @@ -32,12 +33,11 @@ templ DomainCards(domains []configuration.Domain) {
</div>
}

templ WhoisDetail(whois configuration.WhoisCache) {
if (configuration.WhoisCache{}) == whois {
<div class="text-center text-2xl">No WHOIS data available</div>
return
}
templ WhoisError(err error) {
<div class="text-error">{ fmt.Sprintf("WHOIS Cache Miss. %v", err) }</div>
}

templ WhoisDetail(whois configuration.WhoisCache) {
<div class="flex flex-col">
@WhoisDetailItem("Registrar", whois.WhoisInfo.Registrar.Name)
@WhoisDetailItem("Name Servers", strings.Join(whois.WhoisInfo.Domain.NameServers, ", "))
Expand Down

0 comments on commit 63ff81c

Please sign in to comment.