diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | CHANGELOG.adoc | 19 | ||||
| -rw-r--r-- | README.md | 38 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | lilin.go | 2 | ||||
| -rw-r--r-- | notif_config.go | 56 | ||||
| -rw-r--r-- | notif_config_test.go | 51 | ||||
| -rw-r--r-- | server_config_test.go | 27 | ||||
| -rw-r--r-- | testdata/server_config/ok/etc/lilin/lilin.cfg | 14 | ||||
| -rw-r--r-- | testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg | 2 | ||||
| -rw-r--r-- | worker.go | 86 |
12 files changed, 270 insertions, 32 deletions
@@ -1,7 +1,7 @@ ## SPDX-License-Identifier: GPL-3.0-only ## SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info> -/_www/doc/index.html +/_www/doc/*.html /cover.html /cover.out /lilin diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c1e45aa..38d3154 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,13 +1,10 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info> -= lilin releases changelog += lilin release changelog :sectanchors: :toc: -Change log of each release of the lilin software. -The latest release log is put on the top. - Legend, * 🪵: Breaking changes @@ -15,6 +12,20 @@ Legend, * 🌼: Enhancement * 💧: Chores + +[#lilin_v0_2_0] +== lilin v0.2.0 (2026-xx-xx) + +**🪵 all: rename notif field "webhook_url" to "remote_url" + +These changes is to minimize duplicate field on "notif" section, so +different kind of notif can use the same "remote_url" value. + +**🌱 all: support sending notification to SMTP server (email)** + +In the main configuration, lilin.cfg, user now can add "notif" section with +kind "smtp" to send notification using user's email. + [#lilin_v0_1_0] == lilin v0.1.0 (2025-12-27) @@ -112,7 +112,7 @@ timeout = 60s ``` Sample of service configuration that monitor TCP service at -127.0.0.1:5432 every 90 seconds with timeout 30 seconds, +127.0.0.1:5432 every 90 seconds with 30 seconds timeout, ``` [service] @@ -127,18 +127,21 @@ timeout = 30s Lilin support sending notification to, - Mattermost using incoming webhook. +- SMTP server (email). See the next section on how to use the notification. ### Mattermost incoming webhook +<!--{{{--> + In the main configuration, add the section "notif" with the following format, ``` [notif] kind = mattermost -webhook_url = # The incoming webhook URL. +remote_url = # The incoming webhook URL. channel = # The channel where the notification will be placed. down_template = # Message template when service is down. up_template = # Message template when service is up. @@ -170,6 +173,37 @@ will be rendered as 2025-09-26 06:38:11 +0000 UTC: Service http-server is down: 503 Service Unavailable ``` +<!--}}}--> + +### SMTP server + +<!--{{{--> + +``` +[notif] +kind = smtp +remote_url = <scheme "://" (domain | ip_address [":" port])> +user = +password = +recipient = +``` + +The `remote_url` define the SMTP server address. +The scheme in `remote_url` can be set either to, + +- smtps: implicit TLS, connect to port 465 by default +- smtp+starttls: explicit STARTTLS, connect to port 587 by default. + +The `user` field define the sender email address. + +The `password` field define the password for authentication, on behalf of +the `user`. + +The `recipient` is the email address that will receives the notification. +This field can be defined multiple times. + +<!--}}}--> + ## Links [Project page](https://sr.ht/~shulhan/lilin). @@ -8,6 +8,7 @@ go 1.24.0 require git.sr.ht/~shulhan/pakakeh.go v0.60.2 require ( + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect @@ -2,6 +2,8 @@ git.sr.ht/~shulhan/pakakeh.go v0.60.2 h1:ZSRE77lYm+mkhvg9pSrxCIO81ydbqt93qbsWuZJ git.sr.ht/~shulhan/pakakeh.go v0.60.2/go.mod h1:1MkKXbLZRHTcnheeSEbRpGztkym4Yxzh90ep+jCxbDc= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= @@ -12,5 +14,7 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= @@ -9,7 +9,7 @@ import ( ) // Version latest released version. -const Version = `0.1.0` +const Version = `0.2.0` // defTimeout define default timeout for service and client connection. const defTimeout = 5 * time.Second diff --git a/notif_config.go b/notif_config.go index 7fa7d2d..c3ed43c 100644 --- a/notif_config.go +++ b/notif_config.go @@ -11,43 +11,75 @@ import ( const ( notifKindMattermost = `mattermost` + notifKindSMTP = `smtp` ) type NotifConfig struct { - webhookURL *url.URL - downTmpl *template.Template - upTmpl *template.Template + remoteURL *url.URL + + downTmpl *template.Template + upTmpl *template.Template + + Kind string `ini:"notif::kind"` + + // RemoteURL define the remote server to send notification. + // For mattermost this is the webhook URL. + // For smtp this is the domain or IP of SMTP server. + RemoteURL string `ini:"notif::remote_url"` + + // Channel that receive the mattermost message. + Channel string `ini:"notif::channel"` + + // User for authentication with SMTP server. + User string `ini:"notif::user"` + + // Password for authentication with SMTP server. + Password string `ini:"notif::password"` - Kind string `ini:"notif::kind"` - WebhookURL string `ini:"notif::webhook_url"` - Channel string `ini:"notif::channel"` DownTemplate string `ini:"notif::down_template"` UpTemplate string `ini:"notif::up_template"` + + // Recipient of email. + Recipient []string `ini:"notif::recipient"` } func (notifConfig *NotifConfig) init() (err error) { + var logp = `notifConfig.init` + switch notifConfig.Kind { case notifKindMattermost: - notifConfig.webhookURL, err = url.Parse(notifConfig.WebhookURL) + notifConfig.remoteURL, err = url.Parse(notifConfig.RemoteURL) if err != nil { - return fmt.Errorf(`invalid WebhookURL %q: %s`, - notifConfig.WebhookURL, err) + return fmt.Errorf(`invalid RemoteURL %q: %s`, + notifConfig.RemoteURL, err) + } + case notifKindSMTP: + if notifConfig.User == `` { + return fmt.Errorf(`%s: %s: empty user`, logp, notifConfig.Kind) + } + if notifConfig.Password == `` { + return fmt.Errorf(`%s: %s: empty password`, logp, notifConfig.Kind) + } + if len(notifConfig.Recipient) == 0 { + return fmt.Errorf(`%s: %s: empty recipient`, logp, notifConfig.Kind) } default: - return fmt.Errorf(`unknown notif kind: %s`, notifConfig.Kind) + return fmt.Errorf(`%s: unknown notif kind %q`, logp, notifConfig.Kind) } if notifConfig.DownTemplate != "" { notifConfig.downTmpl = template.New(`down`) notifConfig.downTmpl, err = notifConfig.downTmpl.Parse(notifConfig.DownTemplate) if err != nil { - return fmt.Errorf(`failed to parse down_template: %s`, err) + return fmt.Errorf(`%s: %s: failed to parse down_template: %s`, + logp, notifConfig.Kind, err) } } if notifConfig.UpTemplate != "" { notifConfig.upTmpl = template.New(`up`) notifConfig.upTmpl, err = notifConfig.upTmpl.Parse(notifConfig.UpTemplate) if err != nil { - return fmt.Errorf(`failed to parse up_template: %s`, err) + return fmt.Errorf(`%s: %s: failed to parse up_template: %s`, + logp, notifConfig.Kind, err) } } return nil diff --git a/notif_config_test.go b/notif_config_test.go new file mode 100644 index 0000000..b7990b1 --- /dev/null +++ b/notif_config_test.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info> + +package lilin + +import ( + "testing" + + "git.sr.ht/~shulhan/pakakeh.go/lib/test" +) + +func TestNotifConfig_init(t *testing.T) { + listCase := []struct { + expError string + config NotifConfig + }{{ + config: NotifConfig{ + Kind: notifKindSMTP, + }, + expError: `notifConfig.init: smtp: empty user`, + }, { + config: NotifConfig{ + Kind: notifKindSMTP, + User: `john.doe@example.local`, + }, + expError: `notifConfig.init: smtp: empty password`, + }, { + config: NotifConfig{ + Kind: notifKindSMTP, + User: `john.doe@example.local`, + Password: `dummy`, + }, + expError: `notifConfig.init: smtp: empty recipient`, + }, { + config: NotifConfig{ + Kind: notifKindSMTP, + User: `john.doe@example.local`, + Password: `dummy`, + Recipient: []string{ + `jane.doe@example.local`, + }, + }, + }} + for _, tc := range listCase { + err := tc.config.init() + if err != nil { + test.Assert(t, ``, tc.expError, err.Error()) + continue + } + } +} diff --git a/server_config_test.go b/server_config_test.go index bb30e5c..bd17b5e 100644 --- a/server_config_test.go +++ b/server_config_test.go @@ -16,15 +16,15 @@ func TestServerConfig_init(t *testing.T) { exp ServerConfig } - var webhookURL12000 *url.URL - var webhookURL12001 *url.URL + var remoteURL12000 *url.URL + var remoteURL12001 *url.URL var err error - webhookURL12000, err = url.Parse(`http://127.0.0.1:12000`) + remoteURL12000, err = url.Parse(`http://127.0.0.1:12000`) if err != nil { t.Fatal(err) } - webhookURL12001, err = url.Parse(`http://127.0.0.1:12001`) + remoteURL12001, err = url.Parse(`http://127.0.0.1:12001`) if err != nil { t.Fatal(err) } @@ -39,18 +39,29 @@ func TestServerConfig_init(t *testing.T) { Address: `127.0.0.1:12121`, Notifs: []*NotifConfig{{ Kind: `mattermost`, - WebhookURL: `http://127.0.0.1:12000`, + RemoteURL: `http://127.0.0.1:12000`, Channel: `chan_1`, UpTemplate: `Service {{.ID}} is up again!`, DownTemplate: `Service {{.ID}} is down: {{.Error}}`, - webhookURL: webhookURL12000, + remoteURL: remoteURL12000, }, { Kind: `mattermost`, - WebhookURL: `http://127.0.0.1:12001`, + RemoteURL: `http://127.0.0.1:12001`, Channel: `chan_2`, UpTemplate: `Service {{.ID}} is alive!`, DownTemplate: `Service {{.ID}} is down: {{.Error}}`, - webhookURL: webhookURL12001, + remoteURL: remoteURL12001, + }, { + Kind: `smtp`, + RemoteURL: `smtps://127.0.0.1:4650`, + User: `john.doe@example.local`, + Password: `dummy`, + Recipient: []string{ + `jane@example.local`, + `kent@example.local`, + }, + UpTemplate: `Service {{.ID}} is alive!`, + DownTemplate: `Service {{.ID}} is down: {{.Error}}`, }}, }, }} diff --git a/testdata/server_config/ok/etc/lilin/lilin.cfg b/testdata/server_config/ok/etc/lilin/lilin.cfg index ef179d4..901d9e6 100644 --- a/testdata/server_config/ok/etc/lilin/lilin.cfg +++ b/testdata/server_config/ok/etc/lilin/lilin.cfg @@ -6,14 +6,24 @@ address = 127.0.0.1:12121 [notif] kind = mattermost -webhook_url = http://127.0.0.1:12000 +remote_url = http://127.0.0.1:12000 channel = chan_1 down_template = Service {{.ID}} is down: {{.Error}} up_template = Service {{.ID}} is up again! [notif] kind = mattermost -webhook_url = http://127.0.0.1:12001 +remote_url = http://127.0.0.1:12001 channel = chan_2 down_template = Service {{.ID}} is down: {{.Error}} up_template = Service {{.ID}} is alive! + +[notif] +kind = smtp +remote_url = smtps://127.0.0.1:4650 +user = john.doe@example.local +password = dummy +recipient = jane@example.local +recipient = kent@example.local +down_template = Service {{.ID}} is down: {{.Error}} +up_template = Service {{.ID}} is alive! diff --git a/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg b/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg index 3c479f3..2225609 100644 --- a/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg +++ b/testdata/worker/pushNotifMattermost/etc/lilin/lilin.cfg @@ -6,7 +6,7 @@ address = 127.0.0.1:12121 [notif] kind = mattermost -webhook_url = http://127.0.0.1:6330/mattermost +remote_url = http://127.0.0.1:6330/mattermost channel = test_webhook down_template = Service {{.ID}} is down: {{.Error}} up_template = Service {{.ID}} is up again! @@ -6,6 +6,7 @@ package lilin import ( "bytes" "encoding/json" + "fmt" "io" "log" "net/http" @@ -14,7 +15,9 @@ import ( "strings" "time" + "git.sr.ht/~shulhan/pakakeh.go/lib/email" "git.sr.ht/~shulhan/pakakeh.go/lib/ini" + "git.sr.ht/~shulhan/pakakeh.go/lib/smtp" ) // worker contains the report of all services. @@ -174,6 +177,8 @@ func (wrk *worker) handlePushNotif() { switch notifConfig.Kind { case notifKindMattermost: wrk.pushNotifMattermost(notifConfig, &scanReport) + case notifKindSMTP: + wrk.pushNotifSMTP(notifConfig, &scanReport) } } } @@ -213,7 +218,7 @@ func (wrk *worker) pushNotifMattermost( var req = &http.Request{ Method: http.MethodPost, - URL: notifConfig.webhookURL, + URL: notifConfig.remoteURL, Header: http.Header{ `Content-Type`: []string{ `application/json`, @@ -242,3 +247,82 @@ func (wrk *worker) pushNotifMattermost( log.Printf(`%s: fail with status code %d: %s`, logp, resp.StatusCode, body) } + +// pushNotifSMTP send notification through SMTP. +func (wrk *worker) pushNotifSMTP(notifConfig *NotifConfig, scanReport *ScanReport) { + var logp = `pushNotifSMTP` + var msg = email.Message{} + var err error + + err = msg.SetFrom(notifConfig.User) + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + + for _, recipient := range notifConfig.Recipient { + err = msg.AddTo(recipient) + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + } + + var subject string + var text bytes.Buffer + if scanReport.Success { + subject = fmt.Sprintf(`[lilin] Service %s is up!`, scanReport.ID) + err = notifConfig.upTmpl.Execute(&text, scanReport) + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + } else { + subject = fmt.Sprintf(`[lilin] Service %s is down!`, scanReport.ID) + err = notifConfig.downTmpl.Execute(&text, scanReport) + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + } + + msg.SetSubject(subject) + + err = msg.SetBodyText(text.Bytes()) + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + + var data []byte + data, err = msg.Pack() + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + + var mailtx = smtp.NewMailTx(notifConfig.User, notifConfig.Recipient, data) + + var clientOpts = smtp.ClientOptions{ + ServerURL: notifConfig.RemoteURL, + AuthUser: notifConfig.User, + AuthPass: notifConfig.Password, + AuthMechanism: smtp.SaslMechanismPlain, + } + var smtpc *smtp.Client + smtpc, err = smtp.NewClient(clientOpts) + if err != nil { + log.Printf(`%s: %s`, logp, err) + return + } + + _, err = smtpc.MailTx(mailtx) + if err != nil { + log.Printf(`%s: %s`, logp, err) + } + + _, err = smtpc.Quit() + if err != nil { + log.Printf(`%s: %s`, logp, err) + } +} |
