2022-10-13 13:14:56 +02:00
|
|
|
// A bridge between ntfy and Alertmanager
|
2022-10-09 14:19:48 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2022-10-13 13:08:54 +02:00
|
|
|
"crypto/sha512"
|
|
|
|
"crypto/subtle"
|
2023-01-16 14:35:53 +01:00
|
|
|
_ "embed"
|
2022-10-10 02:42:13 +02:00
|
|
|
"encoding/base64"
|
2022-10-09 19:50:46 +02:00
|
|
|
"encoding/json"
|
2022-10-10 01:20:18 +02:00
|
|
|
"flag"
|
2022-10-09 19:50:46 +02:00
|
|
|
"fmt"
|
2022-10-09 14:19:48 +02:00
|
|
|
"net/http"
|
2023-01-16 14:35:53 +01:00
|
|
|
"os"
|
2022-10-09 14:19:48 +02:00
|
|
|
"strings"
|
2022-10-09 19:50:46 +02:00
|
|
|
"time"
|
2022-10-09 14:19:48 +02:00
|
|
|
|
|
|
|
"git.xenrox.net/~xenrox/go-log"
|
2023-03-08 16:16:21 +01:00
|
|
|
"git.xenrox.net/~xenrox/ntfy-alertmanager/cache"
|
2023-07-12 14:56:48 +02:00
|
|
|
"git.xenrox.net/~xenrox/ntfy-alertmanager/config"
|
2022-10-09 19:50:46 +02:00
|
|
|
"golang.org/x/text/cases"
|
|
|
|
"golang.org/x/text/language"
|
2022-10-09 14:19:48 +02:00
|
|
|
)
|
|
|
|
|
2023-02-16 13:06:21 +01:00
|
|
|
var version = "dev"
|
2023-01-16 14:35:53 +01:00
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
type bridge struct {
|
2023-07-12 14:56:48 +02:00
|
|
|
cfg *config.Config
|
2022-10-09 19:50:46 +02:00
|
|
|
logger *log.Logger
|
2023-03-08 16:16:21 +01:00
|
|
|
cache cache.Cache
|
2023-02-20 13:27:41 +01:00
|
|
|
client *httpClient
|
2022-10-09 19:50:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type payload struct {
|
2023-02-12 14:40:32 +01:00
|
|
|
Status string `json:"status"`
|
|
|
|
Alerts []alert `json:"alerts"`
|
|
|
|
GroupLabels map[string]string `json:"groupLabels"`
|
|
|
|
CommonLabels map[string]string `json:"commonLabels"`
|
|
|
|
CommonAnnotations map[string]string `json:"commonAnnotations"`
|
|
|
|
ExternalURL string `json:"externalURL"`
|
2022-10-09 19:50:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type alert struct {
|
2023-02-12 14:40:32 +01:00
|
|
|
Status string `json:"status"`
|
|
|
|
Labels map[string]string `json:"labels"`
|
|
|
|
Annotations map[string]string `json:"annotations"`
|
2023-03-08 16:16:21 +01:00
|
|
|
Fingerprint string `json:"fingerprint"`
|
2022-10-09 19:50:46 +02:00
|
|
|
}
|
|
|
|
|
2022-10-13 01:17:29 +02:00
|
|
|
type notification struct {
|
2023-03-09 17:02:05 +01:00
|
|
|
title string
|
|
|
|
body string
|
|
|
|
priority string
|
|
|
|
tags string
|
|
|
|
icon string
|
|
|
|
emailAddress string
|
2023-07-07 21:37:11 +02:00
|
|
|
call string
|
2023-03-09 17:02:05 +01:00
|
|
|
silenceBody string
|
|
|
|
fingerprint string
|
|
|
|
status string
|
2022-10-13 01:17:29 +02:00
|
|
|
}
|
|
|
|
|
2023-07-07 23:20:03 +02:00
|
|
|
type ntfyError struct {
|
|
|
|
Error string `json:"error"`
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) singleAlertNotifications(p *payload) []*notification {
|
2023-02-03 17:35:38 +01:00
|
|
|
var notifications []*notification
|
|
|
|
for _, alert := range p.Alerts {
|
2023-03-08 22:05:15 +01:00
|
|
|
contains, err := br.cache.Contains(alert.Fingerprint, alert.Status)
|
|
|
|
if err != nil {
|
|
|
|
br.logger.Errorf("Failed to lookup alert %q in cache: %v", alert.Fingerprint, err)
|
|
|
|
}
|
|
|
|
if contains {
|
|
|
|
br.logger.Debugf("Alert %q skipped: Still in cache", alert.Fingerprint)
|
2023-02-04 08:26:23 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-02-03 17:35:38 +01:00
|
|
|
n := new(notification)
|
2023-02-12 16:50:18 +01:00
|
|
|
n.fingerprint = alert.Fingerprint
|
|
|
|
n.status = alert.Status
|
2023-02-03 17:35:38 +01:00
|
|
|
|
|
|
|
// create title
|
|
|
|
n.title = fmt.Sprintf("[%s]", strings.ToUpper(alert.Status))
|
|
|
|
if name, ok := alert.Labels["alertname"]; ok {
|
|
|
|
n.title = fmt.Sprintf("%s %s", n.title, name)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, value := range p.GroupLabels {
|
|
|
|
n.title = fmt.Sprintf("%s %s", n.title, value)
|
|
|
|
}
|
|
|
|
|
|
|
|
// create body
|
|
|
|
n.body = "Labels:\n"
|
2023-02-21 14:37:26 +01:00
|
|
|
sortedLabelKeys := sortKeys(alert.Labels)
|
|
|
|
for _, key := range sortedLabelKeys {
|
|
|
|
n.body = fmt.Sprintf("%s%s = %s\n", n.body, key, alert.Labels[key])
|
2023-02-03 17:35:38 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
n.body += "\nAnnotations:\n"
|
|
|
|
for key, value := range alert.Annotations {
|
|
|
|
n.body = fmt.Sprintf("%s%s = %s\n", n.body, key, value)
|
|
|
|
}
|
|
|
|
|
|
|
|
var tags []string
|
2023-02-12 17:08:58 +01:00
|
|
|
if alert.Status == "resolved" {
|
2023-07-12 14:56:48 +02:00
|
|
|
tags = append(tags, br.cfg.Resolved.Tags...)
|
|
|
|
n.icon = br.cfg.Resolved.Icon
|
2023-02-12 17:08:58 +01:00
|
|
|
}
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
n.emailAddress = br.cfg.Ntfy.EmailAddress
|
|
|
|
n.call = br.cfg.Ntfy.Call
|
2023-03-09 17:02:05 +01:00
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
for _, labelName := range br.cfg.Labels.Order {
|
2023-02-03 17:35:38 +01:00
|
|
|
val, ok := alert.Labels[labelName]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
labelConfig, ok := br.cfg.Labels.Label[fmt.Sprintf("%s:%s", labelName, val)]
|
2023-02-03 17:35:38 +01:00
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if n.priority == "" {
|
|
|
|
n.priority = labelConfig.Priority
|
|
|
|
}
|
|
|
|
|
2023-02-20 13:08:59 +01:00
|
|
|
if n.icon == "" {
|
|
|
|
n.icon = labelConfig.Icon
|
|
|
|
}
|
|
|
|
|
2023-03-09 17:02:05 +01:00
|
|
|
if n.emailAddress == "" {
|
2023-07-12 14:56:48 +02:00
|
|
|
n.emailAddress = labelConfig.EmailAddress
|
2023-03-09 17:02:05 +01:00
|
|
|
}
|
|
|
|
|
2023-07-07 21:37:11 +02:00
|
|
|
if n.call == "" {
|
2023-07-12 14:56:48 +02:00
|
|
|
n.call = labelConfig.Call
|
2023-07-07 21:37:11 +02:00
|
|
|
}
|
|
|
|
|
2023-02-03 17:35:38 +01:00
|
|
|
for _, val := range labelConfig.Tags {
|
|
|
|
if !sliceContains(tags, val) {
|
|
|
|
tags = append(tags, val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
n.tags = strings.Join(tags, ",")
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
if br.cfg.Am.SilenceDuration != 0 && alert.Status == "firing" {
|
2023-02-21 13:57:21 +01:00
|
|
|
if br.cfg.BaseURL == "" {
|
|
|
|
br.logger.Error("Failed to create silence action: No base-url set")
|
2023-02-12 16:32:34 +01:00
|
|
|
} else {
|
|
|
|
// I could not convince ntfy to accept an Action with a body which contains
|
|
|
|
// a json with more than one key. Instead the json will be base64 encoded
|
|
|
|
// and sent to the ntfy-alertmanager silences endpoint, that operates as
|
|
|
|
// a proxy and will do the Alertmanager API request.
|
|
|
|
s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: alert.Labels}
|
|
|
|
b, err := json.Marshal(s)
|
|
|
|
if err != nil {
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Errorf("Failed to create silence action: %v", err)
|
2023-02-12 16:32:34 +01:00
|
|
|
}
|
2023-02-12 03:04:17 +01:00
|
|
|
|
2023-02-12 16:32:34 +01:00
|
|
|
n.silenceBody = base64.StdEncoding.EncodeToString(b)
|
2023-02-12 03:04:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-03 17:35:38 +01:00
|
|
|
notifications = append(notifications, n)
|
|
|
|
}
|
|
|
|
|
|
|
|
return notifications
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) multiAlertNotification(p *payload) *notification {
|
2022-11-02 22:43:34 +01:00
|
|
|
n := new(notification)
|
|
|
|
|
|
|
|
// create title
|
|
|
|
count := len(p.Alerts)
|
|
|
|
title := fmt.Sprintf("[%s", strings.ToUpper(p.Status))
|
|
|
|
if p.Status == "firing" {
|
|
|
|
title = fmt.Sprintf("%s:%d", title, count)
|
|
|
|
}
|
|
|
|
|
|
|
|
title += "]"
|
|
|
|
for _, value := range p.GroupLabels {
|
|
|
|
title = fmt.Sprintf("%s %s", title, value)
|
|
|
|
}
|
|
|
|
n.title = title
|
|
|
|
|
|
|
|
// create body
|
|
|
|
var body string
|
|
|
|
c := cases.Title(language.English)
|
|
|
|
|
|
|
|
for _, alert := range p.Alerts {
|
|
|
|
alertBody := fmt.Sprintf("%s\nLabels:\n", c.String(alert.Status))
|
2023-02-21 14:37:26 +01:00
|
|
|
|
|
|
|
sortedLabelKeys := sortKeys(alert.Labels)
|
|
|
|
for _, key := range sortedLabelKeys {
|
|
|
|
alertBody = fmt.Sprintf("%s%s = %s\n", alertBody, key, alert.Labels[key])
|
2022-11-02 22:43:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
alertBody += "Annotations:\n"
|
|
|
|
for key, value := range alert.Annotations {
|
|
|
|
alertBody = fmt.Sprintf("%s%s = %s\n", alertBody, key, value)
|
|
|
|
}
|
|
|
|
|
|
|
|
alertBody += "\n"
|
|
|
|
|
|
|
|
body += alertBody
|
|
|
|
}
|
|
|
|
n.body = body
|
|
|
|
|
|
|
|
var tags []string
|
2023-02-12 17:08:58 +01:00
|
|
|
if p.Status == "resolved" {
|
2023-07-12 14:56:48 +02:00
|
|
|
tags = append(tags, br.cfg.Resolved.Tags...)
|
|
|
|
n.icon = br.cfg.Resolved.Icon
|
2023-02-12 17:08:58 +01:00
|
|
|
}
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
n.emailAddress = br.cfg.Ntfy.EmailAddress
|
|
|
|
n.call = br.cfg.Ntfy.Call
|
2023-03-09 17:02:05 +01:00
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
for _, labelName := range br.cfg.Labels.Order {
|
2022-11-02 22:43:34 +01:00
|
|
|
val, ok := p.CommonLabels[labelName]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
labelConfig, ok := br.cfg.Labels.Label[fmt.Sprintf("%s:%s", labelName, val)]
|
2022-11-02 22:43:34 +01:00
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2023-02-21 14:02:19 +01:00
|
|
|
if n.priority == "" {
|
|
|
|
n.priority = labelConfig.Priority
|
2022-11-02 22:43:34 +01:00
|
|
|
}
|
|
|
|
|
2023-02-20 13:08:59 +01:00
|
|
|
if n.icon == "" {
|
|
|
|
n.icon = labelConfig.Icon
|
|
|
|
}
|
|
|
|
|
2023-03-09 17:02:05 +01:00
|
|
|
if n.emailAddress == "" {
|
2023-07-12 14:56:48 +02:00
|
|
|
n.emailAddress = labelConfig.EmailAddress
|
2023-03-09 17:02:05 +01:00
|
|
|
}
|
|
|
|
|
2023-07-07 21:37:11 +02:00
|
|
|
if n.call == "" {
|
2023-07-12 14:56:48 +02:00
|
|
|
n.call = labelConfig.Call
|
2023-07-07 21:37:11 +02:00
|
|
|
}
|
|
|
|
|
2022-11-02 22:43:34 +01:00
|
|
|
for _, val := range labelConfig.Tags {
|
|
|
|
if !sliceContains(tags, val) {
|
|
|
|
tags = append(tags, val)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-21 14:02:19 +01:00
|
|
|
n.tags = strings.Join(tags, ",")
|
2022-11-02 22:43:34 +01:00
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
if br.cfg.Am.SilenceDuration != 0 && p.Status == "firing" {
|
2023-02-21 13:57:21 +01:00
|
|
|
if br.cfg.BaseURL == "" {
|
|
|
|
br.logger.Error("Failed to create silence action: No base-url set")
|
2023-02-12 16:32:34 +01:00
|
|
|
} else {
|
2023-02-12 14:28:24 +01:00
|
|
|
|
2023-02-12 16:32:34 +01:00
|
|
|
s := &silenceBody{AlertManagerURL: p.ExternalURL, Labels: p.CommonLabels}
|
|
|
|
b, err := json.Marshal(s)
|
|
|
|
if err != nil {
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Errorf("Failed to create silence action: %v", err)
|
2023-02-12 16:32:34 +01:00
|
|
|
}
|
2023-02-12 14:28:24 +01:00
|
|
|
|
2023-02-12 16:32:34 +01:00
|
|
|
n.silenceBody = base64.StdEncoding.EncodeToString(b)
|
|
|
|
}
|
2023-02-12 14:28:24 +01:00
|
|
|
}
|
|
|
|
|
2022-11-02 22:43:34 +01:00
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) publish(n *notification) error {
|
2023-07-12 14:56:48 +02:00
|
|
|
req, err := http.NewRequest(http.MethodPost, br.cfg.Ntfy.Topic, strings.NewReader(n.body))
|
2022-10-13 01:17:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-02-21 11:50:50 +01:00
|
|
|
// ntfy authentication
|
2023-07-12 14:56:48 +02:00
|
|
|
if br.cfg.Ntfy.Password != "" && br.cfg.Ntfy.User != "" {
|
|
|
|
req.SetBasicAuth(br.cfg.Ntfy.User, br.cfg.Ntfy.Password)
|
|
|
|
} else if br.cfg.Ntfy.AccessToken != "" {
|
|
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", br.cfg.Ntfy.AccessToken))
|
2022-10-13 01:17:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("X-Title", n.title)
|
|
|
|
|
|
|
|
if n.priority != "" {
|
|
|
|
req.Header.Set("X-Priority", n.priority)
|
|
|
|
}
|
|
|
|
|
2023-02-20 13:08:59 +01:00
|
|
|
if n.icon != "" {
|
|
|
|
req.Header.Set("X-Icon", n.icon)
|
|
|
|
}
|
|
|
|
|
2022-10-13 01:17:29 +02:00
|
|
|
if n.tags != "" {
|
|
|
|
req.Header.Set("X-Tags", n.tags)
|
|
|
|
}
|
|
|
|
|
2023-03-09 17:02:05 +01:00
|
|
|
if n.emailAddress != "" {
|
|
|
|
req.Header.Set("X-Email", n.emailAddress)
|
|
|
|
}
|
|
|
|
|
2023-07-07 21:37:11 +02:00
|
|
|
if n.call != "" {
|
|
|
|
req.Header.Set("X-Call", n.call)
|
|
|
|
}
|
|
|
|
|
2023-02-12 03:04:17 +01:00
|
|
|
if n.silenceBody != "" {
|
2023-02-21 13:57:21 +01:00
|
|
|
url := br.cfg.BaseURL + "/silences"
|
2023-02-12 03:04:17 +01:00
|
|
|
|
|
|
|
var authString string
|
2023-02-21 13:57:21 +01:00
|
|
|
if br.cfg.User != "" && br.cfg.Password != "" {
|
|
|
|
auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", br.cfg.User, br.cfg.Password)))
|
2023-02-12 03:04:17 +01:00
|
|
|
authString = fmt.Sprintf(", headers.Authorization=Basic %s", auth)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Set("Actions", fmt.Sprintf("http, Silence, %s, method=POST, body=%s%s", url, n.silenceBody, authString))
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
resp, err := br.client.Do(req)
|
2022-10-13 01:17:29 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2023-07-07 23:20:03 +02:00
|
|
|
var ntfyError ntfyError
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&ntfyError); err != nil {
|
|
|
|
br.logger.Debugf("Publish: failed to decode error: %v", err)
|
|
|
|
return fmt.Errorf("ntfy: received status code %d", resp.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("ntfy: %s (status code %d)", ntfyError.Error, resp.StatusCode)
|
2022-10-13 01:17:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) handleWebhooks(w http.ResponseWriter, r *http.Request) {
|
2022-10-09 19:50:46 +02:00
|
|
|
defer r.Body.Close()
|
|
|
|
|
2022-10-09 20:03:55 +02:00
|
|
|
if r.Method != http.MethodPost {
|
2022-10-10 22:59:05 +02:00
|
|
|
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Debugf("illegal HTTP method: expected %q, got %q", "POST", r.Method)
|
2022-10-09 20:03:55 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
contentType := r.Header.Get("Content-Type")
|
|
|
|
if contentType != "application/json" {
|
2022-10-10 22:59:05 +02:00
|
|
|
http.Error(w, "Only application/json allowed", http.StatusUnsupportedMediaType)
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Debugf("illegal content type: %s", contentType)
|
2022-10-09 20:03:55 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-10-09 19:50:46 +02:00
|
|
|
var event payload
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Debug(err)
|
2022-10-09 19:50:46 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
if br.logger.Level() == log.Debug {
|
|
|
|
br.logger.Debugf("Received alert %+v", event)
|
2023-02-08 15:31:06 +01:00
|
|
|
}
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
if br.cfg.AlertMode == config.Single {
|
2023-02-21 13:57:21 +01:00
|
|
|
notifications := br.singleAlertNotifications(&event)
|
2023-02-03 17:35:38 +01:00
|
|
|
for _, n := range notifications {
|
2023-02-21 13:57:21 +01:00
|
|
|
err := br.publish(n)
|
2023-02-03 17:35:38 +01:00
|
|
|
if err != nil {
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Errorf("Failed to publish notification: %v", err)
|
2023-02-12 16:50:18 +01:00
|
|
|
} else {
|
2023-03-08 22:05:15 +01:00
|
|
|
if err := br.cache.Set(n.fingerprint, n.status); err != nil {
|
|
|
|
br.logger.Errorf("Failed to set alert %q in cache: %v", n.fingerprint, err)
|
|
|
|
}
|
2023-02-03 17:35:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2023-02-21 13:57:21 +01:00
|
|
|
notification := br.multiAlertNotification(&event)
|
|
|
|
err := br.publish(notification)
|
2023-02-03 17:35:38 +01:00
|
|
|
if err != nil {
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Errorf("Failed to publish notification: %v", err)
|
2023-02-03 17:35:38 +01:00
|
|
|
}
|
2022-10-09 19:50:46 +02:00
|
|
|
}
|
2022-10-09 14:19:48 +02:00
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) basicAuthMiddleware(handler http.HandlerFunc) http.HandlerFunc {
|
2022-10-10 19:55:33 +02:00
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
user, pass, ok := r.BasicAuth()
|
|
|
|
if !ok {
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Debug("basic auth failure")
|
2022-10-10 19:55:33 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-10-13 13:08:54 +02:00
|
|
|
inputUserHash := sha512.Sum512([]byte(user))
|
|
|
|
inputPassHash := sha512.Sum512([]byte(pass))
|
2023-02-21 13:57:21 +01:00
|
|
|
configUserHash := sha512.Sum512([]byte(br.cfg.User))
|
|
|
|
configPassHash := sha512.Sum512([]byte(br.cfg.Password))
|
2022-10-13 13:08:54 +02:00
|
|
|
|
|
|
|
validUser := subtle.ConstantTimeCompare(inputUserHash[:], configUserHash[:])
|
|
|
|
validPass := subtle.ConstantTimeCompare(inputPassHash[:], configPassHash[:])
|
|
|
|
|
|
|
|
if validUser != 1 || validPass != 1 {
|
2022-10-10 22:59:05 +02:00
|
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Debug("basic auth: wrong user or password")
|
2022-10-10 19:55:33 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
handler(w, r)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-21 13:57:21 +01:00
|
|
|
func (br *bridge) runCleanup() {
|
2023-02-04 08:26:23 +01:00
|
|
|
for {
|
2023-07-12 14:56:48 +02:00
|
|
|
time.Sleep(br.cfg.Cache.CleanupInterval)
|
2023-02-21 13:57:21 +01:00
|
|
|
br.logger.Info("Pruning cache")
|
2023-03-08 16:16:21 +01:00
|
|
|
br.cache.Cleanup()
|
2023-02-04 08:26:23 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-09 14:19:48 +02:00
|
|
|
func main() {
|
2022-10-10 01:20:18 +02:00
|
|
|
var configPath string
|
|
|
|
flag.StringVar(&configPath, "config", "/etc/ntfy-alertmanager/config", "config file path")
|
2023-01-16 14:35:53 +01:00
|
|
|
var showVersion bool
|
|
|
|
flag.BoolVar(&showVersion, "version", false, "Show version and exit")
|
2022-10-10 01:20:18 +02:00
|
|
|
flag.Parse()
|
|
|
|
|
2023-01-16 14:35:53 +01:00
|
|
|
if showVersion {
|
|
|
|
fmt.Println(version)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2022-10-09 14:19:48 +02:00
|
|
|
logger := log.NewDefaultLogger()
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
cfg, err := config.ReadConfig(configPath)
|
2022-10-10 01:20:18 +02:00
|
|
|
if err != nil {
|
2023-02-21 14:00:21 +01:00
|
|
|
logger.Fatalf("Failed to read config: %v", err)
|
2022-10-10 01:20:18 +02:00
|
|
|
}
|
|
|
|
|
2022-10-13 01:15:04 +02:00
|
|
|
if err := logger.SetLevelFromString(cfg.LogLevel); err != nil {
|
2023-02-21 14:00:21 +01:00
|
|
|
logger.Errorf("Failed to parse logging level: %v", err)
|
2022-10-10 01:20:18 +02:00
|
|
|
}
|
|
|
|
|
2023-02-20 13:27:41 +01:00
|
|
|
client := &httpClient{&http.Client{Timeout: time.Second * 3}}
|
2023-02-17 01:45:14 +01:00
|
|
|
|
2023-03-08 22:05:15 +01:00
|
|
|
var c cache.Cache
|
2023-07-12 14:56:48 +02:00
|
|
|
switch cfg.Cache.Type {
|
|
|
|
case config.Memory:
|
|
|
|
c = cache.NewMemoryCache(cfg.Cache.Duration)
|
|
|
|
case config.Redis:
|
2023-03-08 22:05:15 +01:00
|
|
|
var err error
|
2023-07-12 14:56:48 +02:00
|
|
|
c, err = cache.NewRedisCache(cfg.Cache.RedisURL, cfg.Cache.Duration)
|
2023-03-08 22:05:15 +01:00
|
|
|
if err != nil {
|
|
|
|
logger.Fatalf("Failed to create redis cache: %v", err)
|
|
|
|
}
|
|
|
|
}
|
2023-03-08 16:16:21 +01:00
|
|
|
bridge := &bridge{cfg: cfg, logger: logger, cache: c, client: client}
|
2022-10-09 19:50:46 +02:00
|
|
|
|
2023-01-16 14:35:53 +01:00
|
|
|
logger.Infof("Listening on %s, ntfy-alertmanager %s", cfg.HTTPAddress, version)
|
2022-10-10 19:55:33 +02:00
|
|
|
|
|
|
|
if cfg.User != "" && cfg.Password != "" {
|
|
|
|
logger.Info("Enabling HTTP Basic Authentication")
|
2023-02-21 13:57:21 +01:00
|
|
|
http.HandleFunc("/", bridge.basicAuthMiddleware(bridge.handleWebhooks))
|
|
|
|
http.HandleFunc("/silences", bridge.basicAuthMiddleware(bridge.handleSilences))
|
2022-10-10 19:55:33 +02:00
|
|
|
} else {
|
2023-02-21 13:57:21 +01:00
|
|
|
http.HandleFunc("/", bridge.handleWebhooks)
|
|
|
|
http.HandleFunc("/silences", bridge.handleSilences)
|
2022-10-10 19:55:33 +02:00
|
|
|
}
|
|
|
|
|
2023-07-12 14:56:48 +02:00
|
|
|
if cfg.Cache.Type == config.Memory {
|
2023-03-08 22:05:15 +01:00
|
|
|
go bridge.runCleanup()
|
|
|
|
}
|
2022-10-10 01:20:18 +02:00
|
|
|
logger.Fatal(http.ListenAndServe(cfg.HTTPAddress, nil))
|
2022-10-09 14:19:48 +02:00
|
|
|
}
|