Compare commits

...

10 commits

Author SHA1 Message Date
Thorben Günther
e1ae4d2e43
readme: Update explanation for resolved alerts
Priority is supported as well.
2023-09-28 13:48:14 +02:00
Thorben Günther
8868bfa20d
Run gofmt 2023-09-28 13:42:49 +02:00
Thorben Günther
9a030f2902
config: Fix test 2023-09-28 13:41:23 +02:00
Jackson Chen
9b4b135a39
implement priority for resolved alerts 2023-09-28 13:05:28 +02:00
Thorben Günther
3baffc9bef
Send HTTP error code when failing to decode payload 2023-09-28 12:59:10 +02:00
Thorben Günther
d2eef546d5
Remove unnecessary Body.close() calls
The server will close those.
2023-09-22 22:35:16 +02:00
Thorben Günther
86afe915f3
publish: Move fingerprint conversion to config parsing
We only really need to do it once, not every time a new message gets
published.
2023-08-28 00:36:41 +02:00
Thorben Günther
1abacacab4
publish: Improve certificate fingerprint output 2023-08-27 16:53:19 +02:00
Thorben Günther
8f28182111
config.scfg: Add instructions to obtain cert fingerprint 2023-08-25 22:46:41 +02:00
Thorben Günther
ad2bc1fd89
publish: Improve certificate verification
Remove colons and convert to lower case. hex.EncodeToString outputs a
lower case string.
2023-08-25 22:36:00 +02:00
6 changed files with 36 additions and 13 deletions

View file

@ -22,7 +22,7 @@ ntfy-alertmanager has support for setting ntfy [priority], [tags], [icon], [acti
(which can be used to create an Alertmanager silence), [email notifications] and [phone calls]. (which can be used to create an Alertmanager silence), [email notifications] and [phone calls].
Define a decreasing order of labels in the config file and map those labels to tags, priority, an icon or an email address. Define a decreasing order of labels in the config file and map those labels to tags, priority, an icon or an email address.
- For priority and icon the first found value will be chosen. An icon for "resolved" alerts will take precedence. - For priority and icon the first found value will be chosen. Settings for "resolved" alerts will take precedence.
- Tags are added together. - Tags are added together.
### Alertmanager config ### Alertmanager config

View file

@ -40,6 +40,7 @@ labels {
resolved { resolved {
tags "resolved,partying_face" tags "resolved,partying_face"
icon "https://foo.com/resolved.png" icon "https://foo.com/resolved.png"
priority 1
} }
ntfy { ntfy {
@ -53,7 +54,10 @@ ntfy {
access-token foobar access-token foobar
# When using (self signed) certificates that cannot be verified, you can instead specify # When using (self signed) certificates that cannot be verified, you can instead specify
# the SHA512 fingerprint. # the SHA512 fingerprint.
certificate-fingerprint 136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c299 # openssl can be used to obtain it:
# openssl s_client -connect HOST:PORT | openssl x509 -fingerprint -sha512 -noout
# For convenience ntfy-alertmanager will convert the certificate to lower case and remove all colons.
certificate-fingerprint 13:6D:2B:88:9C:57:36:D0:81:B4:B2:9C:79:09:27:62:92:CF:B8:6A:6B:D3:AD:46:35:CB:70:17:EB:99:6E:28:08:2A:B8:C6:79:4B:F6:2E:81:79:41:98:1D:53:C8:07:B3:5C:24:5F:B1:8E:B6:FB:66:B5:DD:B4:D0:5C:29:91
# Forward all messages to the specified email address. # Forward all messages to the specified email address.
email-address foo@bar.com email-address foo@bar.com
# Call the specified number for all alerts. Use `yes` to pick the first of your verified numbers. # Call the specified number for all alerts. Use `yes` to pick the first of your verified numbers.

View file

@ -77,8 +77,9 @@ type alertmanagerConfig struct {
} }
type resolvedConfig struct { type resolvedConfig struct {
Tags []string Tags []string
Icon string Icon string
Priority string
} }
// ReadConfig reads an scfg formatted file and returns the configuration struct. // ReadConfig reads an scfg formatted file and returns the configuration struct.
@ -283,6 +284,9 @@ func ReadConfig(path string) (*Config, error) {
if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil { if err := d.ParseParams(&config.Ntfy.CertFingerprint); err != nil {
return nil, err return nil, err
} }
// hex.EncodeToString outputs a lower case string
config.Ntfy.CertFingerprint = strings.ToLower(strings.ReplaceAll(config.Ntfy.CertFingerprint, ":", ""))
} }
d = ntfyDir.Children.Get("email-address") d = ntfyDir.Children.Get("email-address")
@ -412,6 +416,13 @@ func ReadConfig(path string) (*Config, error) {
return nil, err return nil, err
} }
} }
d = resolvedDir.Children.Get("priority")
if d != nil {
if err := d.ParseParams(&config.Resolved.Priority); err != nil {
return nil, err
}
}
} }
return config, nil return config, nil

View file

@ -41,11 +41,12 @@ labels {
resolved { resolved {
tags "resolved,partying_face" tags "resolved,partying_face"
icon "https://foo.com/resolved.png" icon "https://foo.com/resolved.png"
priority 1
} }
ntfy { ntfy {
topic https://ntfy.sh/alertmanager-alerts topic https://ntfy.sh/alertmanager-alerts
certificate-fingerprint 136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c299 certificate-fingerprint 13:6D:2B:88:9C:57:36:D0:81:B4:B2:9C:79:09:27:62:92:CF:B8:6A:6B:D3:AD:46:35:CB:70:17:EB:99:6E:28:08:2A:B8:C6:79:4B:F6:2E:81:79:41:98:1D:53:C8:07:B3:5C:24:5F:B1:8E:B6:FB:66:B5:DD:B4:D0:5C:29:91
user user user user
password pass password pass
} }
@ -76,7 +77,7 @@ cache {
Topic: "https://ntfy.sh/alertmanager-alerts", Topic: "https://ntfy.sh/alertmanager-alerts",
User: "user", User: "user",
Password: "pass", Password: "pass",
CertFingerprint: "136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c299", CertFingerprint: "136d2b889c5736d081b4b29c7909276292cfb86a6bd3ad4635cb7017eb996e28082ab8c6794bf62e817941981d53c807b35c245fb18eb6fb66b5ddb4d05c2991",
}, },
Labels: labels{Order: []string{"severity", "instance"}, Labels: labels{Order: []string{"severity", "instance"},
Label: map[string]labelConfig{ Label: map[string]labelConfig{
@ -104,8 +105,9 @@ cache {
URL: "https://alertmanager.xenrox.net", URL: "https://alertmanager.xenrox.net",
}, },
Resolved: resolvedConfig{ Resolved: resolvedConfig{
Tags: []string{"resolved", "partying_face"}, Tags: []string{"resolved", "partying_face"},
Icon: "https://foo.com/resolved.png", Icon: "https://foo.com/resolved.png",
Priority: "1",
}, },
} }

14
main.go
View file

@ -117,6 +117,7 @@ func (br *bridge) singleAlertNotifications(p *payload) []*notification {
if alert.Status == "resolved" { if alert.Status == "resolved" {
tags = append(tags, br.cfg.Resolved.Tags...) tags = append(tags, br.cfg.Resolved.Tags...)
n.icon = br.cfg.Resolved.Icon n.icon = br.cfg.Resolved.Icon
n.priority = br.cfg.Resolved.Priority
} }
n.emailAddress = br.cfg.Ntfy.EmailAddress n.emailAddress = br.cfg.Ntfy.EmailAddress
@ -226,6 +227,7 @@ func (br *bridge) multiAlertNotification(p *payload) *notification {
if p.Status == "resolved" { if p.Status == "resolved" {
tags = append(tags, br.cfg.Resolved.Tags...) tags = append(tags, br.cfg.Resolved.Tags...)
n.icon = br.cfg.Resolved.Icon n.icon = br.cfg.Resolved.Icon
n.priority = br.cfg.Resolved.Priority
} }
n.emailAddress = br.cfg.Ntfy.EmailAddress n.emailAddress = br.cfg.Ntfy.EmailAddress
@ -349,7 +351,14 @@ func (br *bridge) publish(n *notification) error {
} }
hash := sha512.Sum512(rawCerts[0]) hash := sha512.Sum512(rawCerts[0])
return fmt.Errorf("ntfy certificate fingerprint does not match: expected %q, got %q", hex.EncodeToString(hash[:]), configFingerprint) var expectedFingerprint string
for i, b := range hash {
if i != 0 {
expectedFingerprint += ":"
}
expectedFingerprint += fmt.Sprintf("%02X", b)
}
return fmt.Errorf("the ntfy certificate fingerprint (%s) is not set in the config", expectedFingerprint)
} }
tlsCfg.InsecureSkipVerify = true tlsCfg.InsecureSkipVerify = true
@ -377,8 +386,6 @@ func (br *bridge) publish(n *notification) error {
} }
func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) { func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
logger := br.logger.With(slog.String("handler", "/")) logger := br.logger.With(slog.String("handler", "/"))
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
@ -396,6 +403,7 @@ func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
var event payload var event payload
if err := json.NewDecoder(r.Body).Decode(&event); err != nil { if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
http.Error(w, "Failed to parse payload", http.StatusInternalServerError)
logger.Debug("Failed to decode payload", logger.Debug("Failed to decode payload",
slog.String("error", err.Error())) slog.String("error", err.Error()))
return return

View file

@ -38,8 +38,6 @@ type silenceResponse struct {
} }
func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) { func (br *bridge) handleSilences(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
logger := br.logger.With(slog.String("handler", "/silences")) logger := br.logger.With(slog.String("handler", "/silences"))
if r.Method != http.MethodPost { if r.Method != http.MethodPost {