c2382d29a1
Use netip.Addr instead of storing addresses as strings. This requires conversions at the database level and in tests, but is more memory efficient otherwise, and facilitates the following. Parse rate limit exemptions as netip.Prefix. This allows storing IP ranges in the exemption list. Regular IP addresses (entered explicitly or resolved from hostnames) are IPV4/32, denoting a range of one address.
341 lines
10 KiB
Go
341 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"firebase.google.com/go/v4/messaging"
|
|
"github.com/stretchr/testify/require"
|
|
"heckel.io/ntfy/auth"
|
|
)
|
|
|
|
type testAuther struct {
|
|
Allow bool
|
|
}
|
|
|
|
func (t testAuther) Authenticate(_, _ string) (*auth.User, error) {
|
|
return nil, errors.New("not used")
|
|
}
|
|
|
|
func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error {
|
|
if t.Allow {
|
|
return nil
|
|
}
|
|
return errors.New("unauthorized")
|
|
}
|
|
|
|
type testFirebaseSender struct {
|
|
allowed int
|
|
messages []*messaging.Message
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func newTestFirebaseSender(allowed int) *testFirebaseSender {
|
|
return &testFirebaseSender{
|
|
allowed: allowed,
|
|
messages: make([]*messaging.Message, 0),
|
|
}
|
|
}
|
|
|
|
func (s *testFirebaseSender) Send(m *messaging.Message) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if len(s.messages)+1 > s.allowed {
|
|
return errFirebaseQuotaExceeded
|
|
}
|
|
s.messages = append(s.messages, m)
|
|
return nil
|
|
}
|
|
|
|
func (s *testFirebaseSender) Messages() []*messaging.Message {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return append(make([]*messaging.Message, 0), s.messages...)
|
|
}
|
|
|
|
func TestToFirebaseMessage_Keepalive(t *testing.T) {
|
|
m := newKeepaliveMessage("mytopic")
|
|
fbm, err := toFirebaseMessage(m, nil)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "mytopic", fbm.Topic)
|
|
require.Nil(t, fbm.Android)
|
|
require.Equal(t, &messaging.APNSConfig{
|
|
Headers: map[string]string{
|
|
"apns-push-type": "background",
|
|
"apns-priority": "5",
|
|
},
|
|
Payload: &messaging.APNSPayload{
|
|
Aps: &messaging.Aps{
|
|
ContentAvailable: true,
|
|
},
|
|
CustomData: map[string]any{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": m.Event,
|
|
"topic": m.Topic,
|
|
},
|
|
},
|
|
}, fbm.APNS)
|
|
require.Equal(t, map[string]string{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": m.Event,
|
|
"topic": m.Topic,
|
|
}, fbm.Data)
|
|
}
|
|
|
|
func TestToFirebaseMessage_Open(t *testing.T) {
|
|
m := newOpenMessage("mytopic")
|
|
fbm, err := toFirebaseMessage(m, nil)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "mytopic", fbm.Topic)
|
|
require.Nil(t, fbm.Android)
|
|
require.Equal(t, &messaging.APNSConfig{
|
|
Headers: map[string]string{
|
|
"apns-push-type": "background",
|
|
"apns-priority": "5",
|
|
},
|
|
Payload: &messaging.APNSPayload{
|
|
Aps: &messaging.Aps{
|
|
ContentAvailable: true,
|
|
},
|
|
CustomData: map[string]any{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": m.Event,
|
|
"topic": m.Topic,
|
|
},
|
|
},
|
|
}, fbm.APNS)
|
|
require.Equal(t, map[string]string{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": m.Event,
|
|
"topic": m.Topic,
|
|
}, fbm.Data)
|
|
}
|
|
|
|
func TestToFirebaseMessage_Message_Normal_Allowed(t *testing.T) {
|
|
m := newDefaultMessage("mytopic", "this is a message")
|
|
m.Priority = 4
|
|
m.Tags = []string{"tag 1", "tag2"}
|
|
m.Click = "https://google.com"
|
|
m.Icon = "https://ntfy.sh/static/img/ntfy.png"
|
|
m.Title = "some title"
|
|
m.Actions = []*action{
|
|
{
|
|
ID: "123",
|
|
Action: "view",
|
|
Label: "Open page",
|
|
Clear: true,
|
|
URL: "https://ntfy.sh",
|
|
},
|
|
{
|
|
ID: "456",
|
|
Action: "http",
|
|
Label: "Close door",
|
|
URL: "https://door.com/close",
|
|
Method: "PUT",
|
|
Headers: map[string]string{
|
|
"really": "yes",
|
|
},
|
|
},
|
|
}
|
|
m.Attachment = &attachment{
|
|
Name: "some file.jpg",
|
|
Type: "image/jpeg",
|
|
Size: 12345,
|
|
Expires: 98765543,
|
|
URL: "https://example.com/file.jpg",
|
|
}
|
|
fbm, err := toFirebaseMessage(m, &testAuther{Allow: true})
|
|
require.Nil(t, err)
|
|
require.Equal(t, "mytopic", fbm.Topic)
|
|
require.Equal(t, &messaging.AndroidConfig{
|
|
Priority: "high",
|
|
}, fbm.Android)
|
|
require.Equal(t, &messaging.APNSConfig{
|
|
Payload: &messaging.APNSPayload{
|
|
Aps: &messaging.Aps{
|
|
MutableContent: true,
|
|
Alert: &messaging.ApsAlert{
|
|
Title: "some title",
|
|
Body: "this is a message",
|
|
},
|
|
},
|
|
CustomData: map[string]any{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": "message",
|
|
"topic": "mytopic",
|
|
"priority": "4",
|
|
"tags": strings.Join(m.Tags, ","),
|
|
"click": "https://google.com",
|
|
"icon": "https://ntfy.sh/static/img/ntfy.png",
|
|
"title": "some title",
|
|
"message": "this is a message",
|
|
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
|
"encoding": "",
|
|
"attachment_name": "some file.jpg",
|
|
"attachment_type": "image/jpeg",
|
|
"attachment_size": "12345",
|
|
"attachment_expires": "98765543",
|
|
"attachment_url": "https://example.com/file.jpg",
|
|
},
|
|
},
|
|
}, fbm.APNS)
|
|
require.Equal(t, map[string]string{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": "message",
|
|
"topic": "mytopic",
|
|
"priority": "4",
|
|
"tags": strings.Join(m.Tags, ","),
|
|
"click": "https://google.com",
|
|
"icon": "https://ntfy.sh/static/img/ntfy.png",
|
|
"title": "some title",
|
|
"message": "this is a message",
|
|
"actions": `[{"id":"123","action":"view","label":"Open page","clear":true,"url":"https://ntfy.sh"},{"id":"456","action":"http","label":"Close door","clear":false,"url":"https://door.com/close","method":"PUT","headers":{"really":"yes"}}]`,
|
|
"encoding": "",
|
|
"attachment_name": "some file.jpg",
|
|
"attachment_type": "image/jpeg",
|
|
"attachment_size": "12345",
|
|
"attachment_expires": "98765543",
|
|
"attachment_url": "https://example.com/file.jpg",
|
|
}, fbm.Data)
|
|
}
|
|
|
|
func TestToFirebaseMessage_Message_Normal_Not_Allowed(t *testing.T) {
|
|
m := newDefaultMessage("mytopic", "this is a message")
|
|
m.Priority = 5
|
|
fbm, err := toFirebaseMessage(m, &testAuther{Allow: false}) // Not allowed!
|
|
require.Nil(t, err)
|
|
require.Equal(t, "mytopic", fbm.Topic)
|
|
require.Equal(t, &messaging.AndroidConfig{
|
|
Priority: "high",
|
|
}, fbm.Android)
|
|
require.Equal(t, "", fbm.Data["message"])
|
|
require.Equal(t, "", fbm.Data["priority"])
|
|
require.Equal(t, map[string]string{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": "poll_request",
|
|
"topic": "mytopic",
|
|
}, fbm.Data)
|
|
}
|
|
|
|
func TestToFirebaseMessage_PollRequest(t *testing.T) {
|
|
m := newPollRequestMessage("mytopic", "fOv6k1QbCzo6")
|
|
fbm, err := toFirebaseMessage(m, nil)
|
|
require.Nil(t, err)
|
|
require.Equal(t, "mytopic", fbm.Topic)
|
|
require.Nil(t, fbm.Android)
|
|
require.Equal(t, &messaging.APNSConfig{
|
|
Payload: &messaging.APNSPayload{
|
|
Aps: &messaging.Aps{
|
|
MutableContent: true,
|
|
Alert: &messaging.ApsAlert{
|
|
Title: "",
|
|
Body: "New message",
|
|
},
|
|
},
|
|
CustomData: map[string]any{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": "poll_request",
|
|
"topic": "mytopic",
|
|
"message": "New message",
|
|
"poll_id": "fOv6k1QbCzo6",
|
|
},
|
|
},
|
|
}, fbm.APNS)
|
|
require.Equal(t, map[string]string{
|
|
"id": m.ID,
|
|
"time": fmt.Sprintf("%d", m.Time),
|
|
"event": "poll_request",
|
|
"topic": "mytopic",
|
|
"message": "New message",
|
|
"poll_id": "fOv6k1QbCzo6",
|
|
}, fbm.Data)
|
|
}
|
|
|
|
func TestMaybeTruncateFCMMessage(t *testing.T) {
|
|
origMessage := strings.Repeat("this is a long string", 300)
|
|
origFCMMessage := &messaging.Message{
|
|
Topic: "mytopic",
|
|
Data: map[string]string{
|
|
"id": "abcdefg",
|
|
"time": "1641324761",
|
|
"event": "message",
|
|
"topic": "mytopic",
|
|
"priority": "0",
|
|
"tags": "",
|
|
"title": "",
|
|
"message": origMessage,
|
|
},
|
|
Android: &messaging.AndroidConfig{
|
|
Priority: "high",
|
|
},
|
|
}
|
|
origMessageLength := len(origFCMMessage.Data["message"])
|
|
serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage)
|
|
require.Greater(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition
|
|
|
|
truncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage)
|
|
truncatedMessageLength := len(truncatedFCMMessage.Data["message"])
|
|
serializedTruncatedFCMMessage, _ := json.Marshal(truncatedFCMMessage)
|
|
require.Equal(t, fcmMessageLimit, len(serializedTruncatedFCMMessage))
|
|
require.Equal(t, "1", truncatedFCMMessage.Data["truncated"])
|
|
require.NotEqual(t, origMessageLength, truncatedMessageLength)
|
|
}
|
|
|
|
func TestMaybeTruncateFCMMessage_NotTooLong(t *testing.T) {
|
|
origMessage := "not really a long string"
|
|
origFCMMessage := &messaging.Message{
|
|
Topic: "mytopic",
|
|
Data: map[string]string{
|
|
"id": "abcdefg",
|
|
"time": "1641324761",
|
|
"event": "message",
|
|
"topic": "mytopic",
|
|
"priority": "0",
|
|
"tags": "",
|
|
"title": "",
|
|
"message": origMessage,
|
|
},
|
|
}
|
|
origMessageLength := len(origFCMMessage.Data["message"])
|
|
serializedOrigFCMMessage, _ := json.Marshal(origFCMMessage)
|
|
require.LessOrEqual(t, len(serializedOrigFCMMessage), fcmMessageLimit) // Pre-condition
|
|
|
|
notTruncatedFCMMessage := maybeTruncateFCMMessage(origFCMMessage)
|
|
notTruncatedMessageLength := len(notTruncatedFCMMessage.Data["message"])
|
|
serializedNotTruncatedFCMMessage, _ := json.Marshal(notTruncatedFCMMessage)
|
|
require.Equal(t, origMessageLength, notTruncatedMessageLength)
|
|
require.Equal(t, len(serializedOrigFCMMessage), len(serializedNotTruncatedFCMMessage))
|
|
require.Equal(t, "", notTruncatedFCMMessage.Data["truncated"])
|
|
}
|
|
|
|
func TestToFirebaseSender_Abuse(t *testing.T) {
|
|
sender := &testFirebaseSender{allowed: 2}
|
|
client := newFirebaseClient(sender, &testAuther{})
|
|
visitor := newVisitor(newTestConfig(t), newMemTestCache(t), netip.MustParseAddr("1.2.3.4"))
|
|
|
|
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
|
|
require.Equal(t, 1, len(sender.Messages()))
|
|
|
|
require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"}))
|
|
require.Equal(t, 2, len(sender.Messages()))
|
|
|
|
require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"}))
|
|
require.Equal(t, 2, len(sender.Messages()))
|
|
|
|
sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working
|
|
require.Equal(t, errFirebaseTemporarilyBanned, client.Send(visitor, &message{Topic: "mytopic"}))
|
|
require.Equal(t, 0, len(sender.Messages()))
|
|
}
|