server_web_push_test.go 8.0 KB

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