fix: critical bugs and security hardening #19
+17
-6
@@ -2,6 +2,7 @@ package alert
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-upkeep/internal/models"
|
"go-upkeep/internal/models"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
var alertClient = &http.Client{Timeout: 10 * time.Second}
|
var alertClient = &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
Send(title, message string) error
|
Send(ctx context.Context, title, message string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PayloadFunc func(title, message string) ([]byte, error)
|
type PayloadFunc func(title, message string) ([]byte, error)
|
||||||
@@ -25,12 +26,17 @@ type HTTPProvider struct {
|
|||||||
Payload PayloadFunc
|
Payload PayloadFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HTTPProvider) Send(title, message string) error {
|
func (h *HTTPProvider) Send(ctx context.Context, title, message string) error {
|
||||||
body, err := h.Payload(title, message)
|
body, err := h.Payload(title, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resp, err := alertClient.Post(h.URL, "application/json", bytes.NewBuffer(body))
|
req, err := http.NewRequestWithContext(ctx, "POST", h.URL, bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := alertClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -170,7 +176,12 @@ type EmailProvider struct {
|
|||||||
Host, Port, User, Pass, To, From string
|
Host, Port, User, Pass, To, From string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EmailProvider) Send(title, message string) error {
|
func (e *EmailProvider) Send(ctx context.Context, title, message string) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
auth := smtp.PlainAuth("", e.User, e.Pass, e.Host)
|
auth := smtp.PlainAuth("", e.User, e.Pass, e.Host)
|
||||||
msg := []byte("To: " + e.To + "\r\n" +
|
msg := []byte("To: " + e.To + "\r\n" +
|
||||||
"Subject: Go-Upkeep: " + title + "\r\n" +
|
"Subject: Go-Upkeep: " + title + "\r\n" +
|
||||||
@@ -187,9 +198,9 @@ type NtfyProvider struct {
|
|||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NtfyProvider) Send(title, message string) error {
|
func (n *NtfyProvider) Send(ctx context.Context, title, message string) error {
|
||||||
url := strings.TrimRight(n.ServerURL, "/") + "/" + n.Topic
|
url := strings.TrimRight(n.ServerURL, "/") + "/" + n.Topic
|
||||||
req, err := http.NewRequest("POST", url, strings.NewReader(message))
|
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(message))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package alert
|
package alert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"go-upkeep/internal/models"
|
"go-upkeep/internal/models"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -17,7 +18,7 @@ func TestHTTPProviderDiscord(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := GetProvider(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": srv.URL}})
|
p := GetProvider(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": srv.URL}})
|
||||||
if err := p.Send("Test Title", "Test Body"); err != nil {
|
if err := p.Send(context.Background(), "Test Title", "Test Body"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ func TestHTTPProviderSlack(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := GetProvider(models.AlertConfig{Type: "slack", Settings: map[string]string{"url": srv.URL}})
|
p := GetProvider(models.AlertConfig{Type: "slack", Settings: map[string]string{"url": srv.URL}})
|
||||||
if err := p.Send("Alert", "Message"); err != nil {
|
if err := p.Send(context.Background(), "Alert", "Message"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ func TestHTTPProviderWebhook(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := GetProvider(models.AlertConfig{Type: "webhook", Settings: map[string]string{"url": srv.URL}})
|
p := GetProvider(models.AlertConfig{Type: "webhook", Settings: map[string]string{"url": srv.URL}})
|
||||||
if err := p.Send("Title", "Body"); err != nil {
|
if err := p.Send(context.Background(), "Title", "Body"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ func TestHTTPProviderErrorOnHTTP4xx(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := GetProvider(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": srv.URL}})
|
p := GetProvider(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": srv.URL}})
|
||||||
if err := p.Send("Test", "Test"); err == nil {
|
if err := p.Send(context.Background(), "Test", "Test"); err == nil {
|
||||||
t.Fatal("expected error on 403 response")
|
t.Fatal("expected error on 403 response")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +90,7 @@ func TestNtfyProvider(t *testing.T) {
|
|||||||
"url": srv.URL,
|
"url": srv.URL,
|
||||||
"topic": "test",
|
"topic": "test",
|
||||||
}})
|
}})
|
||||||
if err := p.Send("Alert Title", "Alert Body"); err != nil {
|
if err := p.Send(context.Background(), "Alert Title", "Alert Body"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ func TestHTTPProviderTelegram(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := &HTTPProvider{URL: srv.URL, Payload: telegramPayload("12345")}
|
p := &HTTPProvider{URL: srv.URL, Payload: telegramPayload("12345")}
|
||||||
if err := p.Send("Alert", "Down"); err != nil {
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
if received["chat_id"] != "12345" {
|
if received["chat_id"] != "12345" {
|
||||||
@@ -133,7 +134,7 @@ func TestHTTPProviderPagerDuty(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := &HTTPProvider{URL: srv.URL, Payload: pagerdutyPayload("test-key", "critical")}
|
p := &HTTPProvider{URL: srv.URL, Payload: pagerdutyPayload("test-key", "critical")}
|
||||||
if err := p.Send("Alert", "Down"); err != nil {
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
if received["routing_key"] != "test-key" {
|
if received["routing_key"] != "test-key" {
|
||||||
@@ -160,7 +161,7 @@ func TestHTTPProviderPushover(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := &HTTPProvider{URL: srv.URL, Payload: pushoverPayload("app-tok", "user-key")}
|
p := &HTTPProvider{URL: srv.URL, Payload: pushoverPayload("app-tok", "user-key")}
|
||||||
if err := p.Send("Alert", "Down"); err != nil {
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
if received["token"] != "app-tok" {
|
if received["token"] != "app-tok" {
|
||||||
@@ -183,7 +184,7 @@ func TestHTTPProviderGotify(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
p := &HTTPProvider{URL: srv.URL, Payload: gotifyPayload("8")}
|
p := &HTTPProvider{URL: srv.URL, Payload: gotifyPayload("8")}
|
||||||
if err := p.Send("Alert", "Down"); err != nil {
|
if err := p.Send(context.Background(), "Alert", "Down"); err != nil {
|
||||||
t.Fatalf("Send: %v", err)
|
t.Fatalf("Send: %v", err)
|
||||||
}
|
}
|
||||||
if received["title"] != "Alert" || received["message"] != "Down" {
|
if received["title"] != "Alert" || received["message"] != "Down" {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type Engine struct {
|
|||||||
histMu sync.RWMutex
|
histMu sync.RWMutex
|
||||||
histories map[int]*SiteHistory
|
histories map[int]*SiteHistory
|
||||||
|
|
||||||
tokenIndex map[string]int
|
tokenIndex map[string]int // protected by mu
|
||||||
|
|
||||||
probeResultsMu sync.RWMutex
|
probeResultsMu sync.RWMutex
|
||||||
probeResults map[int]map[string]NodeResult
|
probeResults map[int]map[string]NodeResult
|
||||||
@@ -433,6 +433,7 @@ func (e *Engine) handleStatusChange(site models.Site, rawStatus string, code int
|
|||||||
func (e *Engine) triggerAlert(alertID int, title, message string) {
|
func (e *Engine) triggerAlert(alertID int, title, message string) {
|
||||||
cfg, err := e.db.GetAlert(alertID)
|
cfg, err := e.db.GetAlert(alertID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.AddLog(fmt.Sprintf("Failed to load alert config %d: %v", alertID, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
provider := alert.GetProvider(cfg)
|
provider := alert.GetProvider(cfg)
|
||||||
@@ -440,8 +441,9 @@ func (e *Engine) triggerAlert(alertID int, title, message string) {
|
|||||||
go func() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_ = ctx
|
if err := provider.Send(ctx, title, message); err != nil {
|
||||||
_ = provider.Send(title, message)
|
e.AddLog(fmt.Sprintf("Alert send failed (%s): %v", cfg.Name, err))
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user