Skip to content

Commit f2a428e

Browse files
committed
Add Signal notifier auth (Bearer token / HTTP Basic)
1 parent 2e133a1 commit f2a428e

7 files changed

Lines changed: 60 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
All notable changes to Gjallar are documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

6+
## [0.9.0] - 2026-08-20
7+
8+
### Added
9+
10+
- Signal notifier authentication: optional `token` (Bearer) or `user`/`pass`
11+
(HTTP Basic) for gateways that require auth.
12+
613
## [0.8.1] - 2026-07-06
714

815
### Changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ Rules: `> N`, `>= N`, `< N`, `<= N`, `== x`, `!= x`, `~ regex`, `rows > 0` (row
2727
Discord, Slack, Gotify, Pushover, generic webhooks, ...
2828
- Free Mobile SMS API (`type: freemobile`).
2929
- Signal via a [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api)
30-
gateway (`type: signal`: `url` to `/v2/send`, sender `number`, `recipients`).
30+
gateway (`type: signal`: `url` to `/v2/send`, sender `number`, `recipients`;
31+
optional `token` for Bearer auth or `user`/`pass` for HTTP Basic).
3132

3233
A monitor alerts after `failure_threshold` consecutive failures (no flapping noise),
3334
and again on recovery. Open incidents survive restarts: no duplicate alerts.

gjallar.example.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ alerts:
3838
url: "https://signal-api.example.com/v2/send"
3939
number: "+33600000000" # sender registered on the gateway
4040
recipients: ["+33611111111", "+33622222222"]
41+
# If the gateway is protected, set ONE of:
42+
# token: "${SIGNAL_TOKEN}" # Bearer token
43+
# user: "gjallar" # or HTTP Basic
44+
# pass: "${SIGNAL_PASS}"
4145

4246
monitors:
4347
# --- HTTP/HTTPS: status code, body regex, TLS certificate expiry ---

internal/alert/notifiers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func BuildNotifiers(alerts map[string]config.Alert) (map[string]Notifier, error)
2121
case "freemobile":
2222
out[name] = NewFreeMobile(a.User, a.Pass)
2323
case "signal":
24-
out[name] = NewSignal(a.URL, a.Number, a.Recipients)
24+
out[name] = NewSignal(a.URL, a.Number, a.Recipients, a.Token, a.User, a.Pass)
2525
default:
2626
return nil, fmt.Errorf("alert %q: unknown type %q", name, a.Type)
2727
}

internal/alert/signal.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@ import (
1010
)
1111

1212
// Signal sends messages through a signal-cli-rest-api compatible gateway
13-
// (POST {url} with {"message", "number", "recipients"}).
13+
// (POST {url} with {"message", "number", "recipients"}). The gateway may be
14+
// protected: set Token for Bearer auth, or User/Pass for HTTP Basic.
1415
type Signal struct {
1516
URL string // full /v2/send endpoint
1617
Number string // sender number registered on the gateway
1718
Recipients []string
19+
Token string // optional Bearer token
20+
User, Pass string // optional HTTP Basic credentials
1821
Client *http.Client
1922
}
2023

21-
func NewSignal(url, number string, recipients []string) *Signal {
22-
return &Signal{URL: url, Number: number, Recipients: recipients, Client: &http.Client{}}
24+
func NewSignal(url, number string, recipients []string, token, user, pass string) *Signal {
25+
return &Signal{
26+
URL: url, Number: number, Recipients: recipients,
27+
Token: token, User: user, Pass: pass, Client: &http.Client{},
28+
}
2329
}
2430

2531
func (s *Signal) Send(ctx context.Context, title, message string) error {
@@ -36,6 +42,11 @@ func (s *Signal) Send(ctx context.Context, title, message string) error {
3642
return err
3743
}
3844
req.Header.Set("Content-Type", "application/json")
45+
if s.Token != "" {
46+
req.Header.Set("Authorization", "Bearer "+s.Token)
47+
} else if s.User != "" || s.Pass != "" {
48+
req.SetBasicAuth(s.User, s.Pass)
49+
}
3950

4051
resp, err := s.Client.Do(req)
4152
if err != nil {

internal/alert/signal_test.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func TestSignal(t *testing.T) {
2323
}))
2424
defer srv.Close()
2525

26-
s := NewSignal(srv.URL, "+33616400522", []string{"+33689816957", "+33616400522"})
26+
s := NewSignal(srv.URL, "+33616400522", []string{"+33689816957", "+33616400522"}, "", "", "")
2727
if err := s.Send(context.Background(), "[Gjallar] DOWN: web", "web — boom"); err != nil {
2828
t.Fatal(err)
2929
}
@@ -43,6 +43,36 @@ func TestSignal(t *testing.T) {
4343
}
4444
}
4545

46+
func TestSignalAuth(t *testing.T) {
47+
var auth string
48+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49+
auth = r.Header.Get("Authorization")
50+
w.WriteHeader(201)
51+
}))
52+
defer srv.Close()
53+
54+
// Bearer takes precedence.
55+
s := NewSignal(srv.URL, "+336", []string{"+337"}, "tok123", "u", "p")
56+
s.Send(context.Background(), "t", "m")
57+
if auth != "Bearer tok123" {
58+
t.Errorf("bearer: got %q", auth)
59+
}
60+
61+
// Basic when no token.
62+
s = NewSignal(srv.URL, "+336", []string{"+337"}, "", "user", "pass")
63+
s.Send(context.Background(), "t", "m")
64+
if !strings.HasPrefix(auth, "Basic ") {
65+
t.Errorf("basic: got %q", auth)
66+
}
67+
68+
// No auth when nothing configured.
69+
s = NewSignal(srv.URL, "+336", []string{"+337"}, "", "", "")
70+
s.Send(context.Background(), "t", "m")
71+
if auth != "" {
72+
t.Errorf("no-auth: got %q", auth)
73+
}
74+
}
75+
4676
func TestBuildNotifiersSignal(t *testing.T) {
4777
ns, err := BuildNotifiers(map[string]config.Alert{
4878
"sig": {Type: "signal", URL: "http://h/v2/send", Number: "+336", Recipients: []string{"+337"}},

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type Alert struct {
5656
// signal
5757
Number string `yaml:"number"` // sender number registered on the gateway
5858
Recipients []string `yaml:"recipients"` // destination numbers
59+
Token string `yaml:"token"` // optional Bearer token for a protected gateway
5960
}
6061

6162
type Monitor struct {

0 commit comments

Comments
 (0)