server_webpush_test.go 9.6 KB

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