diff --git a/protocol/push_notification_client/client.go b/protocol/push_notification_client/client.go index 0e9580434..59f48680d 100644 --- a/protocol/push_notification_client/client.go +++ b/protocol/push_notification_client/client.go @@ -67,6 +67,19 @@ type PushNotificationInfo struct { Version uint64 } +type SentNotification struct { + PublicKey *ecdsa.PublicKey + InstallationID string + SentAt int64 + MessageID []byte + Success bool + Error protobuf.PushNotificationReport_ErrorType +} + +func (s *SentNotification) HashedPublicKey() []byte { + return common.HashPublicKey(s.PublicKey) +} + type Config struct { // Identity is our identity key Identity *ecdsa.PrivateKey @@ -197,6 +210,35 @@ func (c *Client) Stop() error { return nil } +func (c *Client) queryNotificationInfo(publicKey *ecdsa.PublicKey) error { + // Check if we queried recently + queriedAt, err := c.persistence.GetQueriedAt(publicKey) + if err != nil { + return err + } + // Naively query again if too much time has passed. + // Here it might not be necessary + if time.Now().Unix()-queriedAt > staleQueryTimeInSeconds { + c.config.Logger.Info("querying info") + err := c.QueryPushNotificationInfo(publicKey) + if err != nil { + c.config.Logger.Error("could not query pn info", zap.Error(err)) + return err + } + // This is just horrible, but for now will do, + // the issue is that we don't really know how long it will + // take to reply, as there might be multiple servers + // replying to us. + // The only time we are 100% certain that we can proceed is + // when we have non-stale info for each device, but + // most devices are not going to be registered, so we'd still + // have to wait teh maximum amount of time allowed. + time.Sleep(3 * time.Second) + + } + return nil +} + func (c *Client) HandleMessageSent(sentMessage *common.SentMessage) error { c.config.Logger.Info("sent message", zap.Any("sent message", sentMessage)) if !c.config.SendEnabled { @@ -252,33 +294,10 @@ func (c *Client) HandleMessageSent(sentMessage *common.SentMessage) error { c.config.Logger.Info("actionable messages", zap.Any("message-ids", trackedMessageIDs), zap.Any("installation-ids", installationIDs)) - // Check if we queried recently - queriedAt, err := c.persistence.GetQueriedAt(publicKey) + err := c.queryNotificationInfo(publicKey) if err != nil { return err } - - // Naively query again if too much time has passed. - // Here it might not be necessary - if time.Now().Unix()-queriedAt > staleQueryTimeInSeconds { - c.config.Logger.Info("querying info") - err := c.QueryPushNotificationInfo(publicKey) - if err != nil { - c.config.Logger.Error("could not query pn info", zap.Error(err)) - return err - } - // This is just horrible, but for now will do, - // the issue is that we don't really know how long it will - // take to reply, as there might be multiple servers - // replying to us. - // The only time we are 100% certain that we can proceed is - // when we have non-stale info for each device, but - // most devices are not going to be registered, so we'd still - // have to wait teh maximum amount of time allowed. - time.Sleep(3 * time.Second) - - } - c.config.Logger.Info("queried info") // Retrieve infos info, err := c.GetPushNotificationInfo(publicKey, installationIDs) @@ -380,7 +399,12 @@ func (c *Client) shouldNotifyOn(publicKey *ecdsa.PublicKey, installationID strin } func (c *Client) notifiedOn(publicKey *ecdsa.PublicKey, installationID string, messageID []byte) error { - return c.persistence.NotifiedOn(publicKey, installationID, messageID) + return c.persistence.NotifiedOn(&SentNotification{ + PublicKey: publicKey, + SentAt: time.Now().Unix(), + InstallationID: installationID, + MessageID: messageID, + }) } func (p *Client) mutedChatIDsHashes(chatIDs []string) [][]byte { var mutedChatListHashes [][]byte @@ -850,7 +874,14 @@ func (c *Client) HandlePushNotificationQueryResponse(serverPublicKey *ecdsa.Publ } // HandlePushNotificationResponse should set the request as processed -func (p *Client) HandlePushNotificationResponse(serverKey *ecdsa.PublicKey, response protobuf.PushNotificationResponse) error { +func (c *Client) HandlePushNotificationResponse(serverKey *ecdsa.PublicKey, response protobuf.PushNotificationResponse) error { + messageID := response.MessageId + for _, report := range response.Reports { + err := c.persistence.UpdateNotificationResponse(messageID, report) + if err != nil { + return err + } + } return nil } diff --git a/protocol/push_notification_client/client_test.go b/protocol/push_notification_client/client_test.go index 56b60a70c..11b5aaa03 100644 --- a/protocol/push_notification_client/client_test.go +++ b/protocol/push_notification_client/client_test.go @@ -144,13 +144,14 @@ func (s *ClientSuite) TestBuildPushNotificationRegisterMessageAllowFromContactsO s.client.reader = bytes.NewReader([]byte(expectedUUID)) options := &protobuf.PushNotificationRegistration{ - Version: 1, - AccessToken: expectedUUID, - Token: myDeviceToken, - InstallationId: s.installationID, - Enabled: true, - BlockedChatList: mutedChatListHashes, - AllowedUserList: [][]byte{encryptedToken}, + Version: 1, + AccessToken: expectedUUID, + Token: myDeviceToken, + InstallationId: s.installationID, + AllowFromContactsOnly: true, + Enabled: true, + BlockedChatList: mutedChatListHashes, + AllowedUserList: [][]byte{encryptedToken}, } actualMessage, err := s.client.buildPushNotificationRegistrationMessage(contactIDs, mutedChatList) diff --git a/protocol/push_notification_client/migrations/migrations.go b/protocol/push_notification_client/migrations/migrations.go index 35f6e5633..431630032 100644 --- a/protocol/push_notification_client/migrations/migrations.go +++ b/protocol/push_notification_client/migrations/migrations.go @@ -1,7 +1,7 @@ // Code generated by go-bindata. DO NOT EDIT. // sources: // 1593601729_initial_schema.down.sql (144B) -// 1593601729_initial_schema.up.sql (1.6kB) +// 1593601729_initial_schema.up.sql (1.709kB) // doc.go (382B) package migrations @@ -91,7 +91,7 @@ func _1593601729_initial_schemaDownSql() (*asset, error) { return a, nil } -var __1593601729_initial_schemaUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x94\xc1\x6e\xa3\x30\x10\x86\xef\x3c\xc5\x1c\x1b\x29\x87\xbd\xf7\x44\xa8\x59\x21\x59\xf6\x6e\x62\xa4\xdc\x2c\xaf\x71\x1b\x2b\xac\xe9\xda\xa6\x5a\xde\x7e\x65\x08\x29\xa9\x59\xa8\xda\x5e\x22\x65\xfc\x7b\xf0\x37\xf3\xcf\x64\x7b\x94\x32\x04\x2c\xdd\x61\x04\x45\x0e\x84\x32\x40\xc7\xe2\xc0\x0e\xf0\xdc\xba\x13\x37\x8d\xd7\x8f\x5a\x0a\xaf\x1b\xc3\x65\xad\x95\xf1\xdc\x29\xfb\xa2\xac\x83\xbb\x04\xe0\xb9\xfd\x55\x6b\xc9\xcf\xaa\x83\x1d\xa6\xbb\xfe\x3e\x29\x31\xde\x26\x00\x56\x3d\x69\xe7\x95\x55\x15\xec\x28\xc5\x28\x25\xf0\x80\xf2\xb4\xc4\x0c\xf2\x14\x1f\xd0\xad\x86\x0b\x0f\x05\x61\xd7\x0c\x57\xed\xb7\xa0\xab\x85\xf3\xdc\x2a\x6f\xf5\x9a\x32\x88\x3a\x2e\x9b\xd6\x2c\xa9\x84\x94\xca\x39\xee\x9b\xb3\x32\xc0\xd0\x91\x85\x60\x49\x8a\x9f\x25\xba\x7b\x65\xda\x00\x25\x90\x51\x92\xe3\x22\x63\xb0\x47\x3f\x70\x9a\xa1\x64\x73\x9f\x24\x1f\xa9\xdb\x9f\x56\x59\xad\xd6\xeb\x36\xe8\x22\xcc\xf1\xa8\xe3\xba\x8a\x2f\x45\x6f\xdf\x8e\xda\xaf\x85\xd0\xe6\xb1\x59\x25\x18\x1c\xc2\x97\x24\xda\x38\x2f\xea\x7a\xc8\xad\xab\xbe\x07\x37\x82\xa8\x43\x6f\xbc\x15\xac\xf0\x32\x5f\xa5\xe0\x4e\xdd\x98\x28\x1e\xd7\xe8\xed\x33\xb6\xf1\xd3\xbf\xb6\x7c\xde\x0a\x79\x56\x15\xff\xad\x9c\x13\x4f\x17\x33\x5c\xfe\xcc\xf6\x55\x9e\x84\x9f\xad\xcf\x98\x69\x86\xff\xc2\xf9\x9a\xf6\x96\xa1\xf8\x4e\xe8\x1e\x25\x00\x1f\x85\x70\xe1\x67\x7a\xb0\x8e\xf1\x29\x2b\xf4\xdf\x7b\x0f\xe7\x16\x16\x7a\xbb\xf9\x04\xf1\xb0\xa6\xec\x04\x76\x5c\x5d\x43\x2c\x86\x02\x90\x8d\xf1\x42\x86\xe6\xb9\xfe\x78\x88\xba\xce\xf8\x93\xf2\x5a\x06\xd2\xff\xef\xa7\x2b\xdc\x54\xbf\x6a\xc5\x82\x3c\xa0\x23\xe8\xea\x2f\x5f\x9c\xdf\xe9\x60\x52\xb2\x3c\xeb\x4b\xd3\xb2\xb9\x4f\xfe\x05\x00\x00\xff\xff\x2b\xa6\x15\x32\x40\x06\x00\x00") +var __1593601729_initial_schemaUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x54\xc1\x6e\xe2\x30\x10\xbd\xe7\x2b\xe6\x58\x24\x0e\x7b\xef\x29\x50\xb3\x8a\x64\x39\xbb\x60\x24\x6e\x96\xd7\x99\x36\x16\x59\xa7\x6b\x3b\xd5\xf2\xf7\x2b\x27\x90\x42\x9d\x75\xa4\x96\x0b\x12\x33\xcf\xa3\x79\x6f\x5e\xde\x7a\x4b\x72\x4e\x80\xe7\x2b\x4a\xa0\xd8\x00\x2b\x39\x90\x43\xb1\xe3\x3b\x78\xed\x5c\x2d\x4c\xeb\xf5\xb3\x56\xd2\xeb\xd6\x08\xd5\x68\x34\x5e\x38\xb4\x6f\x68\x1d\x3c\x64\x00\xaf\xdd\xaf\x46\x2b\x71\xc4\x13\xac\x68\xb9\xea\xdf\xb3\x3d\xa5\xcb\x0c\xc0\xe2\x8b\x76\x1e\x2d\x56\xb0\x2a\x4b\x4a\x72\x06\x4f\x64\x93\xef\x29\x87\x4d\x4e\x77\xe4\x16\x23\xa4\x87\x82\xf1\x71\xc2\x88\xfd\x16\x70\x8d\x74\x5e\x58\xf4\x56\xcf\x21\x03\xe8\x24\x54\xdb\x99\x14\x4a\x2a\x85\xce\x09\xdf\x1e\xd1\x00\x27\x07\x1e\x8a\x7b\x56\xfc\xdc\x93\x87\x77\x4e\x0b\x28\x19\xac\x4b\xb6\xa1\xc5\x9a\xc3\x96\xfc\xa0\xf9\x9a\x64\x8b\xc7\x2c\xfb\x8c\x6e\x7f\x3a\xb4\x1a\xe7\x75\x1b\x70\x11\xcd\x4b\xeb\x24\x74\x15\x3f\x8a\x76\x5f\x5e\xb0\xf7\x25\xa1\xcd\x73\x3b\xcb\x60\x70\x88\x48\x41\xb4\x71\x5e\x36\xcd\x30\x5b\x57\xfd\x0d\x6e\x00\xd1\x85\x3e\x78\x2b\x58\xe1\x6d\x5a\xa5\xe0\x4e\xdd\x9a\xa8\x1e\x6b\xf4\x71\x8d\x65\xbc\xfa\x7d\xe5\xf3\x56\xaa\x23\x56\xe2\x37\x3a\x27\x5f\xce\x66\x38\xff\x99\xbc\xab\xaa\xa5\x9f\xd4\xe7\x32\x69\x82\xff\x99\xe7\xfb\xd8\x5b\x0e\xc5\x77\x56\x6e\x49\x06\xf0\x59\x12\x2e\xfc\x5c\x37\xe6\x69\xa4\xac\x50\x4b\x57\x63\xf5\x35\xb7\xf4\x2b\x4d\x48\xe1\xba\xde\x46\x63\x00\x45\x69\x30\x26\x11\x5a\xdb\xda\x44\x62\x44\xa2\x2e\x21\x61\xa4\xc5\x17\xe4\x1d\x32\xd1\x5e\x29\x7b\xc9\xc9\xa1\x16\xcb\x03\xa0\x5a\xe3\xa5\x0a\x4e\x71\x7d\x7b\xa8\xba\x93\xf1\x35\x7a\xad\x82\x66\xff\xa7\x36\x92\xbb\xc6\xcf\xfa\xbe\x60\x4f\xe4\x00\xba\xfa\x2b\x92\x61\x71\x7d\xd7\x92\xa5\x83\x25\xf5\x69\x2e\x1e\xb3\x7f\x01\x00\x00\xff\xff\xed\x10\xc3\xcd\xad\x06\x00\x00") func _1593601729_initial_schemaUpSqlBytes() ([]byte, error) { return bindataRead( @@ -106,8 +106,8 @@ func _1593601729_initial_schemaUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1593601729_initial_schema.up.sql", size: 1600, mode: os.FileMode(0644), modTime: time.Unix(1594978360, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd, 0x57, 0x6b, 0xc6, 0x63, 0x1c, 0x3a, 0x78, 0xa0, 0xc3, 0x12, 0x63, 0xbf, 0x34, 0x6c, 0x86, 0xd2, 0xce, 0x6c, 0xfb, 0xdd, 0xa8, 0x44, 0x5c, 0x0, 0x4e, 0x1a, 0x99, 0xa1, 0xfc, 0xf5, 0x3b}} + info := bindataFileInfo{name: "1593601729_initial_schema.up.sql", size: 1709, mode: os.FileMode(0644), modTime: time.Unix(1595237467, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x35, 0x40, 0x6a, 0x4a, 0x45, 0x37, 0x37, 0x99, 0x97, 0x5, 0xb3, 0x43, 0x6, 0x43, 0xcc, 0x10, 0x32, 0xbc, 0x16, 0xcc, 0xe0, 0xfb, 0x3, 0xa8, 0xce, 0x6a, 0x6b, 0x39, 0xd4, 0xe0, 0xbe, 0xa4}} return a, nil } diff --git a/protocol/push_notification_client/migrations/sql/1593601729_initial_schema.up.sql b/protocol/push_notification_client/migrations/sql/1593601729_initial_schema.up.sql index f970d4ef0..d4e4941e6 100644 --- a/protocol/push_notification_client/migrations/sql/1593601729_initial_schema.up.sql +++ b/protocol/push_notification_client/migrations/sql/1593601729_initial_schema.up.sql @@ -35,8 +35,11 @@ CREATE TABLE IF NOT EXISTS push_notification_client_tracked_messages ( CREATE TABLE IF NOT EXISTS push_notification_client_sent_notifications ( message_id BLOB NOT NULL, public_key BLOB NOT NULL, + hashed_public_key BLOB NOT NULL, installation_id TEXT NOT NULL, sent_at INT NOT NULL, + success BOOLEAN NOT NULL DEFAULT FALSE, + error INT NOT NULL DEFAULT 0, UNIQUE(message_id, public_key, installation_id) ); diff --git a/protocol/push_notification_client/persistence.go b/protocol/push_notification_client/persistence.go index f7ecef168..42fd128c9 100644 --- a/protocol/push_notification_client/persistence.go +++ b/protocol/push_notification_client/persistence.go @@ -42,7 +42,7 @@ func (p *Persistence) GetLastPushNotificationRegistration() (*protobuf.PushNotif return nil, nil, err } for _, pkBytes := range publicKeyBytes { - pk, err := crypto.UnmarshalPubkey(pkBytes) + pk, err := crypto.DecompressPubkey(pkBytes) if err != nil { return nil, nil, err } @@ -63,7 +63,7 @@ func (p *Persistence) SaveLastPushNotificationRegistration(registration *protobu var encodedContactIDs bytes.Buffer var contactIDsBytes [][]byte for _, pk := range contactIDs { - contactIDsBytes = append(contactIDsBytes, crypto.FromECDSAPub(pk)) + contactIDsBytes = append(contactIDsBytes, crypto.CompressPubkey(pk)) } pkEncoder := gob.NewEncoder(&encodedContactIDs) if err := pkEncoder.Encode(contactIDsBytes); err != nil { @@ -271,9 +271,34 @@ func (p *Persistence) ShouldSendNotificationToAllInstallationIDs(publicKey *ecds return count == 0, nil } -func (p *Persistence) NotifiedOn(publicKey *ecdsa.PublicKey, installationID string, messageID []byte) error { - sentAt := time.Now().Unix() - _, err := p.db.Exec(`INSERT INTO push_notification_client_sent_notifications (public_key, installation_id, message_id, sent_at) VALUES (?, ?, ?, ?)`, crypto.CompressPubkey(publicKey), installationID, messageID, sentAt) +func (p *Persistence) NotifiedOn(n *SentNotification) error { + _, err := p.db.Exec(`INSERT INTO push_notification_client_sent_notifications (public_key, installation_id, message_id, sent_at, hashed_public_key) VALUES (?, ?, ?, ?, ?)`, crypto.CompressPubkey(n.PublicKey), n.InstallationID, n.MessageID, n.SentAt, n.HashedPublicKey()) + return err +} + +func (p *Persistence) GetSentNotification(hashedPublicKey []byte, installationID string, messageID []byte) (*SentNotification, error) { + var publicKeyBytes []byte + sentNotification := &SentNotification{ + InstallationID: installationID, + MessageID: messageID, + } + err := p.db.QueryRow(`SELECT sent_at, error, success, public_key FROM push_notification_client_sent_notifications WHERE hashed_public_key = ?`, hashedPublicKey).Scan(&sentNotification.SentAt, &sentNotification.Error, &sentNotification.Success, &publicKeyBytes) + if err != nil { + return nil, err + } + + publicKey, err := crypto.DecompressPubkey(publicKeyBytes) + if err != nil { + return nil, err + } + + sentNotification.PublicKey = publicKey + + return sentNotification, nil +} + +func (p *Persistence) UpdateNotificationResponse(messageID []byte, response *protobuf.PushNotificationReport) error { + _, err := p.db.Exec(`UPDATE push_notification_client_sent_notifications SET success = ?, error = ? WHERE hashed_public_key = ? AND installation_id = ? AND message_id = ? AND NOT success`, response.Success, response.Error, response.PublicKey, response.InstallationId, messageID) return err } diff --git a/protocol/push_notification_client/persistence_test.go b/protocol/push_notification_client/persistence_test.go index 05eb42e3f..385f3cf14 100644 --- a/protocol/push_notification_client/persistence_test.go +++ b/protocol/push_notification_client/persistence_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "testing" + "time" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/suite" @@ -211,6 +212,74 @@ func (s *SQLitePersistenceSuite) TestSaveAndRetrieveInfoWithVersion() { s.Require().Equal(uint64(2), retrievedInfos[0].Version) } +func (s *SQLitePersistenceSuite) TestNotifiedOnAndUpdateNotificationResponse() { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + installationID := "installation-id" + messageID := []byte("message-id") + + sentNotification := &SentNotification{ + PublicKey: &key.PublicKey, + InstallationID: installationID, + MessageID: messageID, + SentAt: time.Now().Unix(), + } + + s.Require().NoError(s.persistence.NotifiedOn(sentNotification)) + + retrievedNotification, err := s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) + + response := &protobuf.PushNotificationReport{ + Success: false, + Error: protobuf.PushNotificationReport_WRONG_TOKEN, + PublicKey: sentNotification.HashedPublicKey(), + InstallationId: installationID, + } + + s.Require().NoError(s.persistence.UpdateNotificationResponse(messageID, response)) + + sentNotification.Error = protobuf.PushNotificationReport_WRONG_TOKEN + + retrievedNotification, err = s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) + + // Update with a successful notification + response = &protobuf.PushNotificationReport{ + Success: true, + PublicKey: sentNotification.HashedPublicKey(), + InstallationId: installationID, + } + + s.Require().NoError(s.persistence.UpdateNotificationResponse(messageID, response)) + + sentNotification.Success = true + sentNotification.Error = protobuf.PushNotificationReport_UNKNOWN_ERROR_TYPE + + retrievedNotification, err = s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) + + // Update with a unsuccessful notification, it should be ignored + response = &protobuf.PushNotificationReport{ + Success: false, + Error: protobuf.PushNotificationReport_WRONG_TOKEN, + PublicKey: sentNotification.HashedPublicKey(), + InstallationId: installationID, + } + + s.Require().NoError(s.persistence.UpdateNotificationResponse(messageID, response)) + + sentNotification.Success = true + sentNotification.Error = protobuf.PushNotificationReport_UNKNOWN_ERROR_TYPE + + retrievedNotification, err = s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) +} + func (s *SQLitePersistenceSuite) TestSaveAndRetrieveRegistration() { // Try with nil first retrievedRegistration, retrievedContactIDs, err := s.persistence.GetLastPushNotificationRegistration()