1
0

server_webpush_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package server
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "github.com/stretchr/testify/require"
  6. "heckel.io/ntfy/user"
  7. "heckel.io/ntfy/util"
  8. "io"
  9. "net/http"
  10. "net/http/httptest"
  11. "net/netip"
  12. "strings"
  13. "sync/atomic"
  14. "testing"
  15. "time"
  16. )
  17. const (
  18. testWebPushEndpoint = "https://updates.push.services.mozilla.com/wpush/v1/AAABBCCCDDEEEFFF"
  19. )
  20. func TestServer_WebPush_Disabled(t *testing.T) {
  21. s := newTestServer(t, newTestConfig(t))
  22. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil)
  23. require.Equal(t, 404, response.Code)
  24. }
  25. func TestServer_WebPush_TopicAdd(t *testing.T) {
  26. s := newTestServer(t, newTestConfigWithWebPush(t))
  27. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil)
  28. require.Equal(t, 200, response.Code)
  29. require.Equal(t, `{"success":true}`+"\n", response.Body.String())
  30. subs, err := s.webPush.SubscriptionsForTopic("test-topic")
  31. require.Nil(t, err)
  32. require.Len(t, subs, 1)
  33. require.Equal(t, subs[0].Endpoint, testWebPushEndpoint)
  34. require.Equal(t, subs[0].P256dh, "p256dh-key")
  35. require.Equal(t, subs[0].Auth, "auth-key")
  36. require.Equal(t, subs[0].UserID, "")
  37. }
  38. func TestServer_WebPush_TopicAdd_InvalidEndpoint(t *testing.T) {
  39. s := newTestServer(t, newTestConfigWithWebPush(t))
  40. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, "https://ddos-target.example.com/webpush"), nil)
  41. require.Equal(t, 400, response.Code)
  42. require.Equal(t, `{"code":40039,"http":400,"error":"invalid request: web push endpoint unknown"}`+"\n", response.Body.String())
  43. }
  44. func TestServer_WebPush_TopicAdd_TooManyTopics(t *testing.T) {
  45. s := newTestServer(t, newTestConfigWithWebPush(t))
  46. topicList := make([]string, 51)
  47. for i := range topicList {
  48. topicList[i] = util.RandomString(5)
  49. }
  50. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, topicList, testWebPushEndpoint), nil)
  51. require.Equal(t, 400, response.Code)
  52. require.Equal(t, `{"code":40040,"http":400,"error":"invalid request: too many web push topic subscriptions"}`+"\n", response.Body.String())
  53. }
  54. func TestServer_WebPush_TopicUnsubscribe(t *testing.T) {
  55. s := newTestServer(t, newTestConfigWithWebPush(t))
  56. addSubscription(t, s, testWebPushEndpoint, "test-topic")
  57. requireSubscriptionCount(t, s, "test-topic", 1)
  58. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{}, testWebPushEndpoint), nil)
  59. require.Equal(t, 200, response.Code)
  60. require.Equal(t, `{"success":true}`+"\n", response.Body.String())
  61. requireSubscriptionCount(t, s, "test-topic", 0)
  62. }
  63. func TestServer_WebPush_TopicSubscribeProtected_Allowed(t *testing.T) {
  64. config := configureAuth(t, newTestConfigWithWebPush(t))
  65. config.AuthDefault = user.PermissionDenyAll
  66. s := newTestServer(t, config)
  67. require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
  68. require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
  69. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
  70. "Authorization": util.BasicAuth("ben", "ben"),
  71. })
  72. require.Equal(t, 200, response.Code)
  73. require.Equal(t, `{"success":true}`+"\n", response.Body.String())
  74. subs, err := s.webPush.SubscriptionsForTopic("test-topic")
  75. require.Nil(t, err)
  76. require.Len(t, subs, 1)
  77. require.True(t, strings.HasPrefix(subs[0].UserID, "u_"))
  78. }
  79. func TestServer_WebPush_TopicSubscribeProtected_Denied(t *testing.T) {
  80. config := configureAuth(t, newTestConfigWithWebPush(t))
  81. config.AuthDefault = user.PermissionDenyAll
  82. s := newTestServer(t, config)
  83. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), nil)
  84. require.Equal(t, 403, response.Code)
  85. requireSubscriptionCount(t, s, "test-topic", 0)
  86. }
  87. func TestServer_WebPush_DeleteAccountUnsubscribe(t *testing.T) {
  88. config := configureAuth(t, newTestConfigWithWebPush(t))
  89. s := newTestServer(t, config)
  90. require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
  91. require.Nil(t, s.userManager.AllowAccess("ben", "test-topic", user.PermissionReadWrite))
  92. response := request(t, s, "POST", "/v1/webpush", payloadForTopics(t, []string{"test-topic"}, testWebPushEndpoint), map[string]string{
  93. "Authorization": util.BasicAuth("ben", "ben"),
  94. })
  95. require.Equal(t, 200, response.Code)
  96. require.Equal(t, `{"success":true}`+"\n", response.Body.String())
  97. requireSubscriptionCount(t, s, "test-topic", 1)
  98. request(t, s, "DELETE", "/v1/account", `{"password":"ben"}`, map[string]string{
  99. "Authorization": util.BasicAuth("ben", "ben"),
  100. })
  101. // should've been deleted with the account
  102. requireSubscriptionCount(t, s, "test-topic", 0)
  103. }
  104. func TestServer_WebPush_Publish(t *testing.T) {
  105. s := newTestServer(t, newTestConfigWithWebPush(t))
  106. var received atomic.Bool
  107. pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  108. _, err := io.ReadAll(r.Body)
  109. require.Nil(t, err)
  110. require.Equal(t, "/push-receive", r.URL.Path)
  111. require.Equal(t, "high", r.Header.Get("Urgency"))
  112. require.Equal(t, "", r.Header.Get("Topic"))
  113. received.Store(true)
  114. }))
  115. defer pushService.Close()
  116. addSubscription(t, s, pushService.URL+"/push-receive", "test-topic")
  117. request(t, s, "POST", "/test-topic", "web push test", nil)
  118. waitFor(t, func() bool {
  119. return received.Load()
  120. })
  121. }
  122. func TestServer_WebPush_Publish_RemoveOnError(t *testing.T) {
  123. s := newTestServer(t, newTestConfigWithWebPush(t))
  124. var received atomic.Bool
  125. pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  126. _, err := io.ReadAll(r.Body)
  127. require.Nil(t, err)
  128. w.WriteHeader(http.StatusGone)
  129. received.Store(true)
  130. }))
  131. defer pushService.Close()
  132. addSubscription(t, s, pushService.URL+"/push-receive", "test-topic", "test-topic-abc")
  133. requireSubscriptionCount(t, s, "test-topic", 1)
  134. requireSubscriptionCount(t, s, "test-topic-abc", 1)
  135. request(t, s, "POST", "/test-topic", "web push test", nil)
  136. waitFor(t, func() bool {
  137. return received.Load()
  138. })
  139. // Receiving the 410 should've caused the publisher to expire all subscriptions on the endpoint
  140. requireSubscriptionCount(t, s, "test-topic", 0)
  141. requireSubscriptionCount(t, s, "test-topic-abc", 0)
  142. }
  143. func TestServer_WebPush_Expiry(t *testing.T) {
  144. s := newTestServer(t, newTestConfigWithWebPush(t))
  145. var received atomic.Bool
  146. pushService := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  147. _, err := io.ReadAll(r.Body)
  148. require.Nil(t, err)
  149. w.WriteHeader(200)
  150. w.Write([]byte(``))
  151. received.Store(true)
  152. }))
  153. defer pushService.Close()
  154. addSubscription(t, s, pushService.URL+"/push-receive", "test-topic")
  155. requireSubscriptionCount(t, s, "test-topic", 1)
  156. _, err := s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-7*24*time.Hour).Unix())
  157. require.Nil(t, err)
  158. s.pruneAndNotifyWebPushSubscriptions()
  159. requireSubscriptionCount(t, s, "test-topic", 1)
  160. waitFor(t, func() bool {
  161. return received.Load()
  162. })
  163. _, err = s.webPush.db.Exec("UPDATE subscription SET updated_at = ?", time.Now().Add(-9*24*time.Hour).Unix())
  164. require.Nil(t, err)
  165. s.pruneAndNotifyWebPushSubscriptions()
  166. waitFor(t, func() bool {
  167. subs, err := s.webPush.SubscriptionsForTopic("test-topic")
  168. require.Nil(t, err)
  169. return len(subs) == 0
  170. })
  171. }
  172. func payloadForTopics(t *testing.T, topics []string, endpoint string) string {
  173. topicsJSON, err := json.Marshal(topics)
  174. require.Nil(t, err)
  175. return fmt.Sprintf(`{
  176. "topics": %s,
  177. "endpoint": "%s",
  178. "p256dh": "p256dh-key",
  179. "auth": "auth-key"
  180. }`, topicsJSON, endpoint)
  181. }
  182. func addSubscription(t *testing.T, s *Server, endpoint string, topics ...string) {
  183. require.Nil(t, s.webPush.UpsertSubscription(endpoint, "kSC3T8aN1JCQxxPdrFLrZg", "BMKKbxdUU_xLS7G1Wh5AN8PvWOjCzkCuKZYb8apcqYrDxjOF_2piggBnoJLQYx9IeSD70fNuwawI3e9Y8m3S3PE", "u_123", netip.MustParseAddr("1.2.3.4"), topics)) // Test auth and p256dh
  184. }
  185. func requireSubscriptionCount(t *testing.T, s *Server, topic string, expectedLength int) {
  186. subs, err := s.webPush.SubscriptionsForTopic(topic)
  187. require.Nil(t, err)
  188. require.Len(t, subs, expectedLength)
  189. }