2023-05-30 19:56:10 +02:00
package server
import (
"encoding/json"
"fmt"
2023-06-02 13:22:54 +02:00
"net/http"
2023-06-02 16:52:35 +02:00
"regexp"
2023-06-13 03:01:43 +02:00
"strings"
2023-06-02 13:22:54 +02:00
2023-05-30 19:56:10 +02:00
"github.com/SherClockHolmes/webpush-go"
2023-11-17 02:54:58 +01:00
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/user"
2023-05-30 19:56:10 +02:00
)
2023-06-02 16:52:35 +02:00
const (
2023-06-13 03:01:43 +02:00
webPushTopicSubscribeLimit = 50
2023-06-02 16:52:35 +02:00
)
2023-06-13 03:01:43 +02:00
var (
webPushAllowedEndpointsPatterns = [ ] string {
"https://*.google.com/" ,
"https://*.googleapis.com/" ,
"https://*.mozilla.com/" ,
"https://*.mozaws.net/" ,
"https://*.windows.com/" ,
"https://*.microsoft.com/" ,
"https://*.apple.com/" ,
}
webPushAllowedEndpointsRegex * regexp . Regexp
)
func init ( ) {
for i , pattern := range webPushAllowedEndpointsPatterns {
webPushAllowedEndpointsPatterns [ i ] = strings . ReplaceAll ( strings . ReplaceAll ( pattern , "." , "\\." ) , "*" , ".+" )
}
allPatterns := fmt . Sprintf ( "^(%s)" , strings . Join ( webPushAllowedEndpointsPatterns , "|" ) )
webPushAllowedEndpointsRegex = regexp . MustCompile ( allPatterns )
}
2023-06-02 16:52:35 +02:00
2023-06-02 13:22:54 +02:00
func ( s * Server ) handleWebPushUpdate ( w http . ResponseWriter , r * http . Request , v * visitor ) error {
2023-06-09 05:09:38 +02:00
req , err := readJSONWithLimit [ apiWebPushUpdateSubscriptionRequest ] ( r . Body , jsonBodyBytesLimit , false )
if err != nil || req . Endpoint == "" || req . P256dh == "" || req . Auth == "" {
2023-05-30 19:56:10 +02:00
return errHTTPBadRequestWebPushSubscriptionInvalid
2023-06-13 03:01:43 +02:00
} else if ! webPushAllowedEndpointsRegex . MatchString ( req . Endpoint ) {
2023-06-02 16:52:35 +02:00
return errHTTPBadRequestWebPushEndpointUnknown
2023-06-09 05:09:38 +02:00
} else if len ( req . Topics ) > webPushTopicSubscribeLimit {
2023-06-02 16:52:35 +02:00
return errHTTPBadRequestWebPushTopicCountTooHigh
}
2023-06-09 05:09:38 +02:00
topics , err := s . topicsFromIDs ( req . Topics ... )
2023-05-30 19:56:10 +02:00
if err != nil {
2023-06-02 13:22:54 +02:00
return err
2023-05-30 19:56:10 +02:00
}
2023-06-02 13:22:54 +02:00
if s . userManager != nil {
2023-06-08 18:20:12 +02:00
u := v . User ( )
2023-06-02 13:22:54 +02:00
for _ , t := range topics {
if err := s . userManager . Authorize ( u , t . ID , user . PermissionRead ) ; err != nil {
logvr ( v , r ) . With ( t ) . Err ( err ) . Debug ( "Access to topic %s not authorized" , t . ID )
return errHTTPForbidden . With ( t )
}
}
2023-05-30 19:56:10 +02:00
}
2023-06-17 03:59:07 +02:00
if err := s . webPush . UpsertSubscription ( req . Endpoint , req . Auth , req . P256dh , v . MaybeUserID ( ) , v . IP ( ) , req . Topics ) ; err != nil {
2023-05-30 19:56:10 +02:00
return err
}
return s . writeJSON ( w , newSuccessResponse ( ) )
}
2023-06-11 03:09:01 +02:00
func ( s * Server ) handleWebPushDelete ( w http . ResponseWriter , r * http . Request , _ * visitor ) error {
req , err := readJSONWithLimit [ apiWebPushUpdateSubscriptionRequest ] ( r . Body , jsonBodyBytesLimit , false )
if err != nil || req . Endpoint == "" {
return errHTTPBadRequestWebPushSubscriptionInvalid
}
if err := s . webPush . RemoveSubscriptionsByEndpoint ( req . Endpoint ) ; err != nil {
return err
}
return s . writeJSON ( w , newSuccessResponse ( ) )
}
2023-05-30 19:56:10 +02:00
func ( s * Server ) publishToWebPushEndpoints ( v * visitor , m * message ) {
2023-05-30 20:23:03 +02:00
subscriptions , err := s . webPush . SubscriptionsForTopic ( m . Topic )
2023-05-30 19:56:10 +02:00
if err != nil {
2023-06-16 22:55:42 +02:00
logvm ( v , m ) . Err ( err ) . With ( v , m ) . Warn ( "Unable to publish web push messages" )
2023-05-30 19:56:10 +02:00
return
}
2023-06-16 22:55:42 +02:00
log . Tag ( tagWebPush ) . With ( v , m ) . Debug ( "Publishing web push message to %d subscribers" , len ( subscriptions ) )
2023-06-08 18:20:12 +02:00
payload , err := json . Marshal ( newWebPushPayload ( fmt . Sprintf ( "%s/%s" , s . config . BaseURL , m . Topic ) , m ) )
if err != nil {
2023-06-16 22:55:42 +02:00
log . Tag ( tagWebPush ) . Err ( err ) . With ( v , m ) . Warn ( "Unable to marshal expiring payload" )
2023-06-08 18:20:12 +02:00
return
}
for _ , subscription := range subscriptions {
2023-06-16 22:55:42 +02:00
if err := s . sendWebPushNotification ( subscription , payload , v , m ) ; err != nil {
log . Tag ( tagWebPush ) . Err ( err ) . With ( v , m , subscription ) . Warn ( "Unable to publish web push message" )
2023-06-08 18:20:12 +02:00
}
2023-06-02 14:45:05 +02:00
}
}
2023-05-30 19:56:10 +02:00
2023-06-10 05:17:48 +02:00
func ( s * Server ) pruneAndNotifyWebPushSubscriptions ( ) {
2023-06-09 03:45:52 +02:00
if s . config . WebPushPublicKey == "" {
return
}
go func ( ) {
2023-06-10 05:17:48 +02:00
if err := s . pruneAndNotifyWebPushSubscriptionsInternal ( ) ; err != nil {
2023-06-09 03:45:52 +02:00
log . Tag ( tagWebPush ) . Err ( err ) . Warn ( "Unable to prune or notify web push subscriptions" )
}
} ( )
}
2023-06-10 05:17:48 +02:00
func ( s * Server ) pruneAndNotifyWebPushSubscriptionsInternal ( ) error {
// Expire old subscriptions
if err := s . webPush . RemoveExpiredSubscriptions ( s . config . WebPushExpiryDuration ) ; err != nil {
return err
}
// Notify subscriptions that will expire soon
subscriptions , err := s . webPush . SubscriptionsExpiring ( s . config . WebPushExpiryWarningDuration )
2023-06-02 14:45:05 +02:00
if err != nil {
2023-06-09 03:45:52 +02:00
return err
2023-06-08 18:20:12 +02:00
} else if len ( subscriptions ) == 0 {
2023-06-09 03:45:52 +02:00
return nil
2023-06-02 14:45:05 +02:00
}
2023-06-08 18:20:12 +02:00
payload , err := json . Marshal ( newWebPushSubscriptionExpiringPayload ( ) )
2023-06-02 14:45:05 +02:00
if err != nil {
2023-06-09 03:45:52 +02:00
return err
2023-06-02 14:45:05 +02:00
}
2023-06-10 05:17:48 +02:00
warningSent := make ( [ ] * webPushSubscription , 0 )
2023-06-09 03:45:52 +02:00
for _ , subscription := range subscriptions {
2023-06-16 22:55:42 +02:00
if err := s . sendWebPushNotification ( subscription , payload ) ; err != nil {
log . Tag ( tagWebPush ) . Err ( err ) . With ( subscription ) . Warn ( "Unable to publish expiry imminent warning" )
2023-06-10 05:17:48 +02:00
continue
2023-06-08 18:20:12 +02:00
}
2023-06-10 05:17:48 +02:00
warningSent = append ( warningSent , subscription )
}
if err := s . webPush . MarkExpiryWarningSent ( warningSent ) ; err != nil {
return err
2023-06-09 03:45:52 +02:00
}
2023-06-10 05:17:48 +02:00
log . Tag ( tagWebPush ) . Debug ( "Expired old subscriptions and published %d expiry imminent warnings" , len ( subscriptions ) )
2023-06-09 03:45:52 +02:00
return nil
2023-06-08 18:20:12 +02:00
}
2023-06-02 14:45:05 +02:00
2023-06-16 22:55:42 +02:00
func ( s * Server ) sendWebPushNotification ( sub * webPushSubscription , message [ ] byte , contexters ... log . Contexter ) error {
log . Tag ( tagWebPush ) . With ( sub ) . With ( contexters ... ) . Debug ( "Sending web push message" )
2023-06-18 03:51:04 +02:00
payload := & webpush . Subscription {
Endpoint : sub . Endpoint ,
Keys : webpush . Keys {
Auth : sub . Auth ,
P256dh : sub . P256dh ,
} ,
}
resp , err := webpush . SendNotification ( message , payload , & webpush . Options {
2023-06-02 14:45:05 +02:00
Subscriber : s . config . WebPushEmailAddress ,
VAPIDPublicKey : s . config . WebPushPublicKey ,
VAPIDPrivateKey : s . config . WebPushPrivateKey ,
2023-06-08 18:20:12 +02:00
Urgency : webpush . UrgencyHigh , // iOS requires this to ensure delivery
2023-06-09 11:32:44 +02:00
TTL : int ( s . config . CacheDuration . Seconds ( ) ) ,
2023-06-02 14:45:05 +02:00
} )
if err != nil {
2023-06-16 22:55:42 +02:00
log . Tag ( tagWebPush ) . With ( sub ) . With ( contexters ... ) . Err ( err ) . Debug ( "Unable to publish web push message, removing endpoint" )
2023-06-09 05:09:38 +02:00
if err := s . webPush . RemoveSubscriptionsByEndpoint ( sub . Endpoint ) ; err != nil {
2023-06-08 18:20:12 +02:00
return err
2023-06-02 14:45:05 +02:00
}
2023-06-08 18:20:12 +02:00
return err
2023-06-02 14:45:05 +02:00
}
2023-06-08 18:20:12 +02:00
if ( resp . StatusCode < 200 || resp . StatusCode > 299 ) && resp . StatusCode != 429 {
2023-06-16 22:55:42 +02:00
log . Tag ( tagWebPush ) . With ( sub ) . With ( contexters ... ) . Field ( "response_code" , resp . StatusCode ) . Debug ( "Unable to publish web push message, unexpected response" )
2023-06-09 05:09:38 +02:00
if err := s . webPush . RemoveSubscriptionsByEndpoint ( sub . Endpoint ) ; err != nil {
2023-06-08 18:20:12 +02:00
return err
2023-06-02 14:45:05 +02:00
}
2023-06-16 22:55:42 +02:00
return errHTTPInternalErrorWebPushUnableToPublish . With ( sub ) . With ( contexters ... )
2023-06-02 14:45:05 +02:00
}
2023-06-08 18:20:12 +02:00
return nil
2023-05-30 19:56:10 +02:00
}