2023-05-29 17:57:21 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2023-06-02 13:22:54 +02:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-05-29 17:57:21 +02:00
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2023-05-30 20:23:03 +02:00
|
|
|
"strings"
|
2023-05-29 17:57:21 +02:00
|
|
|
"sync/atomic"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/SherClockHolmes/webpush-go"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"heckel.io/ntfy/user"
|
|
|
|
"heckel.io/ntfy/util"
|
|
|
|
)
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
const (
|
|
|
|
defaultEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
|
|
|
|
)
|
|
|
|
|
2023-06-02 13:22:54 +02:00
|
|
|
func TestServer_WebPush_TopicAdd(t *testing.T) {
|
2023-05-29 17:57:21 +02:00
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), nil)
|
2023-05-29 17:57:21 +02:00
|
|
|
require.Equal(t, 200, response.Code)
|
|
|
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
|
|
|
|
2023-05-30 20:23:03 +02:00
|
|
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
2023-05-31 18:02:04 +02:00
|
|
|
require.Nil(t, err)
|
2023-05-29 17:57:21 +02:00
|
|
|
|
|
|
|
require.Len(t, subs, 1)
|
2023-06-02 16:52:35 +02:00
|
|
|
require.Equal(t, subs[0].BrowserSubscription.Endpoint, defaultEndpoint)
|
2023-05-29 17:57:21 +02:00
|
|
|
require.Equal(t, subs[0].BrowserSubscription.Keys.P256dh, "p256dh-key")
|
|
|
|
require.Equal(t, subs[0].BrowserSubscription.Keys.Auth, "auth-key")
|
2023-05-30 20:23:03 +02:00
|
|
|
require.Equal(t, subs[0].UserID, "")
|
2023-05-29 17:57:21 +02:00
|
|
|
}
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) {
|
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil)
|
2023-06-02 16:52:35 +02:00
|
|
|
require.Equal(t, 400, response.Code)
|
|
|
|
require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) {
|
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
|
|
|
topicList := make([]string, 51)
|
|
|
|
for i := range topicList {
|
|
|
|
topicList[i] = util.RandomString(5)
|
|
|
|
}
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, topicList, defaultEndpoint), nil)
|
2023-06-02 16:52:35 +02:00
|
|
|
require.Equal(t, 400, response.Code)
|
|
|
|
require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String())
|
|
|
|
}
|
|
|
|
|
2023-06-02 13:22:54 +02:00
|
|
|
func TestServer_WebPush_TopicUnsubscribe(t *testing.T) {
|
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
addSubscription(t, s, "test-topic", defaultEndpoint)
|
2023-06-02 13:22:54 +02:00
|
|
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{}, defaultEndpoint), nil)
|
2023-06-02 13:22:54 +02:00
|
|
|
require.Equal(t, 200, response.Code)
|
|
|
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
|
|
|
|
|
|
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
|
|
|
}
|
|
|
|
|
2023-05-29 17:57:21 +02:00
|
|
|
func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
|
|
|
|
config := configureAuth(t, newTestConfigWithWebPush(t))
|
|
|
|
config.AuthDefault = user.PermissionDenyAll
|
|
|
|
s := newTestServer(t, config)
|
|
|
|
|
|
|
|
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
|
|
|
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), map[string]string{
|
2023-05-29 17:57:21 +02:00
|
|
|
"Authorization": util.BasicAuth("ben", "ben"),
|
|
|
|
})
|
|
|
|
require.Equal(t, 200, response.Code)
|
|
|
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
|
|
|
|
2023-05-30 20:23:03 +02:00
|
|
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
|
|
|
require.Nil(t, err)
|
2023-05-29 17:57:21 +02:00
|
|
|
require.Len(t, subs, 1)
|
2023-05-30 20:23:03 +02:00
|
|
|
require.True(t, strings.HasPrefix(subs[0].UserID, "u_"))
|
2023-05-29 17:57:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) {
|
|
|
|
config := configureAuth(t, newTestConfigWithWebPush(t))
|
|
|
|
config.AuthDefault = user.PermissionDenyAll
|
|
|
|
s := newTestServer(t, config)
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), nil)
|
2023-05-29 17:57:21 +02:00
|
|
|
require.Equal(t, 403, response.Code)
|
|
|
|
|
|
|
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
|
|
|
|
config := configureAuth(t, newTestConfigWithWebPush(t))
|
|
|
|
s := newTestServer(t, config)
|
|
|
|
|
|
|
|
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
|
|
|
require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
|
|
|
|
|
2023-06-08 20:30:19 +02:00
|
|
|
response := request(t, s, "PUT", "/v1/account/webpush", payloadForTopics(t, []string{"test-topic"}, defaultEndpoint), map[string]string{
|
2023-05-29 17:57:21 +02:00
|
|
|
"Authorization": util.BasicAuth("ben", "ben"),
|
|
|
|
})
|
|
|
|
|
|
|
|
require.Equal(t, 200, response.Code)
|
|
|
|
require.Equal(t, `{"success":true}`+"\n", response.Body.String())
|
|
|
|
|
|
|
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
|
|
|
|
|
|
|
request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{
|
|
|
|
"Authorization": util.BasicAuth("ben", "ben"),
|
|
|
|
})
|
|
|
|
// should've been deleted with the account
|
|
|
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestServer_WebPush_Publish(t *testing.T) {
|
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
|
|
|
var received atomic.Bool
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-05-29 17:57:21 +02:00
|
|
|
_, err := io.ReadAll(r.Body)
|
|
|
|
require.Nil(t, err)
|
|
|
|
require.Equal(t, "/push-receive", r.URL.Path)
|
|
|
|
require.Equal(t, "high", r.Header.Get("Urgency"))
|
|
|
|
require.Equal(t, "", r.Header.Get("Topic"))
|
|
|
|
received.Store(true)
|
|
|
|
}))
|
2023-06-02 16:52:35 +02:00
|
|
|
defer pushService.Close()
|
2023-05-29 17:57:21 +02:00
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
addSubscription(t, s, "test-topic", pushService.URL+"/push-receive")
|
2023-05-29 17:57:21 +02:00
|
|
|
|
|
|
|
request(t, s, "PUT", "/test-topic", "web push test", nil)
|
|
|
|
|
|
|
|
waitFor(t, func() bool {
|
|
|
|
return received.Load()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-09 03:45:52 +02:00
|
|
|
func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) {
|
2023-05-29 17:57:21 +02:00
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
|
|
|
var received atomic.Bool
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-05-29 17:57:21 +02:00
|
|
|
_, err := io.ReadAll(r.Body)
|
|
|
|
require.Nil(t, err)
|
|
|
|
// Gone
|
|
|
|
w.WriteHeader(410)
|
|
|
|
received.Store(true)
|
|
|
|
}))
|
2023-06-02 16:52:35 +02:00
|
|
|
defer pushService.Close()
|
2023-05-29 17:57:21 +02:00
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
addSubscription(t, s, "test-topic", pushService.URL+"/push-receive")
|
|
|
|
addSubscription(t, s, "test-topic-abc", pushService.URL+"/push-receive")
|
2023-05-29 17:57:21 +02:00
|
|
|
|
|
|
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
|
|
|
requireSubscriptionCount(t, s, "test-topic-abc", 1)
|
|
|
|
|
|
|
|
request(t, s, "PUT", "/test-topic", "web push test", nil)
|
|
|
|
|
|
|
|
waitFor(t, func() bool {
|
|
|
|
return received.Load()
|
|
|
|
})
|
|
|
|
|
|
|
|
// Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint
|
|
|
|
|
|
|
|
requireSubscriptionCount(t, s, "test-topic", 0)
|
|
|
|
requireSubscriptionCount(t, s, "test-topic-abc", 0)
|
|
|
|
}
|
|
|
|
|
2023-06-02 14:45:05 +02:00
|
|
|
func TestServer_WebPush_Expiry(t *testing.T) {
|
|
|
|
s := newTestServer(t, newTestConfigWithWebPush(t))
|
|
|
|
|
|
|
|
var received atomic.Bool
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-06-02 14:45:05 +02:00
|
|
|
_, err := io.ReadAll(r.Body)
|
|
|
|
require.Nil(t, err)
|
|
|
|
w.WriteHeader(200)
|
|
|
|
w.Write([]byte(``))
|
|
|
|
received.Store(true)
|
|
|
|
}))
|
2023-06-02 16:52:35 +02:00
|
|
|
defer pushService.Close()
|
2023-06-02 14:45:05 +02:00
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
addSubscription(t, s, "test-topic", pushService.URL+"/push-receive")
|
2023-06-02 14:45:05 +02:00
|
|
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
|
|
|
|
|
|
|
_, err := s.webPush.db.Exec("UPDATE subscriptions SET updated_at = datetime('now', '-7 days')")
|
|
|
|
require.Nil(t, err)
|
|
|
|
|
2023-06-09 03:45:52 +02:00
|
|
|
s.pruneOrNotifyWebPushSubscriptions()
|
2023-06-02 14:45:05 +02:00
|
|
|
requireSubscriptionCount(t, s, "test-topic", 1)
|
|
|
|
|
|
|
|
waitFor(t, func() bool {
|
|
|
|
return received.Load()
|
|
|
|
})
|
|
|
|
|
|
|
|
_, err = s.webPush.db.Exec("UPDATE subscriptions SET updated_at = datetime('now', '-8 days')")
|
|
|
|
require.Nil(t, err)
|
|
|
|
|
2023-06-09 03:45:52 +02:00
|
|
|
s.pruneOrNotifyWebPushSubscriptions()
|
|
|
|
waitFor(t, func() bool {
|
|
|
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
|
|
|
require.Nil(t, err)
|
|
|
|
return len(subs) == 0
|
|
|
|
})
|
2023-06-02 14:45:05 +02:00
|
|
|
}
|
|
|
|
|
2023-06-02 16:52:35 +02:00
|
|
|
func payloadForTopics(t *testing.T, topics []string, endpoint string) string {
|
2023-06-02 14:45:05 +02:00
|
|
|
topicsJSON, err := json.Marshal(topics)
|
2023-06-02 13:22:54 +02:00
|
|
|
require.Nil(t, err)
|
|
|
|
|
|
|
|
return fmt.Sprintf(`{
|
|
|
|
"topics": %s,
|
|
|
|
"browser_subscription":{
|
2023-06-02 16:52:35 +02:00
|
|
|
"endpoint": "%s",
|
2023-06-02 13:22:54 +02:00
|
|
|
"keys": {
|
|
|
|
"p256dh": "p256dh-key",
|
|
|
|
"auth": "auth-key"
|
|
|
|
}
|
|
|
|
}
|
2023-06-02 16:52:35 +02:00
|
|
|
}`, topicsJSON, endpoint)
|
2023-06-02 13:22:54 +02:00
|
|
|
}
|
|
|
|
|
2023-05-29 17:57:21 +02:00
|
|
|
func addSubscription(t *testing.T, s *Server, topic string, url string) {
|
2023-06-02 13:22:54 +02:00
|
|
|
err := s.webPush.AddSubscription(topic, "", webpush.Subscription{
|
|
|
|
Endpoint: url,
|
|
|
|
Keys: webpush.Keys{
|
|
|
|
// connected to a local test VAPID key, not a leak!
|
|
|
|
Auth: "kSC3T8aN1JCQxxPdrFLrZg",
|
|
|
|
P256dh: "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE",
|
2023-05-29 17:57:21 +02:00
|
|
|
},
|
|
|
|
})
|
2023-05-31 18:02:04 +02:00
|
|
|
require.Nil(t, err)
|
2023-05-29 17:57:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) {
|
2023-05-30 20:23:03 +02:00
|
|
|
subs, err := s.webPush.SubscriptionsForTopic("test-topic")
|
2023-05-31 18:02:04 +02:00
|
|
|
require.Nil(t, err)
|
2023-05-29 17:57:21 +02:00
|
|
|
require.Len(t, subs, expectedLength)
|
|
|
|
}
|