From 3164e40e5ca9cf6b80b92ac2a51c4cafae3d6ccf Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 25 Jan 2017 17:51:24 +0100 Subject: [PATCH 01/32] Allow devices to register for push notifications Signed-off-by: Joas Schilling --- appinfo/database.xml | 43 ++++++ appinfo/routes.php | 2 + lib/AppInfo/Application.php | 7 + lib/Controller/PushController.php | 229 ++++++++++++++++++++++++++++++ 4 files changed, 281 insertions(+) create mode 100644 lib/Controller/PushController.php diff --git a/appinfo/database.xml b/appinfo/database.xml index 66ecdcbdf..c9bdab860 100755 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -4,6 +4,7 @@ true false utf8 + *dbprefix*notifications @@ -118,4 +119,46 @@
+ + + *dbprefix*notifications_pushtokens + + + uid + text + true + 64 + + + token + integer + 0 + true + 4 + + + devicepublickey + text + true + 128 + + + pushtokenhash + text + true + 128 + + + + oc_notifpushtoken + true + + uid + + + token + + + +
diff --git a/appinfo/routes.php b/appinfo/routes.php index 6c11dc32f..a79e4c8d3 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -24,5 +24,7 @@ ['name' => 'Endpoint#listNotifications', 'url' => '/api/{apiVersion}/notifications', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)']], ['name' => 'Endpoint#getNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']], ['name' => 'Endpoint#deleteNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']], + ['name' => 'Push#registerDevice', 'url' => '/api/3/push', 'verb' => 'POST'], + ['name' => 'Push#removeDevice', 'url' => '/api/3/push', 'verb' => 'DELETE'], ], ]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index c1577a0ac..92f97961e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -21,9 +21,11 @@ namespace OCA\Notifications\AppInfo; +use OC\Authentication\Token\IProvider; use OCA\Notifications\App; use OCA\Notifications\Capabilities; use OCA\Notifications\Controller\EndpointController; +use OCP\AppFramework\IAppContainer; use OCP\Util; class Application extends \OCP\AppFramework\App { @@ -33,6 +35,11 @@ public function __construct() { $container->registerAlias('EndpointController', EndpointController::class); $container->registerCapability(Capabilities::class); + + // FIXME this is for automatic DI because it is not in DIContainer + $container->registerService(IProvider::class, function(IAppContainer $c) { + return $c->getServer()->query(IProvider::class); + }); } public function register() { diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php new file mode 100644 index 000000000..2e4f4ad7c --- /dev/null +++ b/lib/Controller/PushController.php @@ -0,0 +1,229 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Notifications\Controller; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OC\Security\IdentityProof\Crypto; +use OC\Security\IdentityProof\Manager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\OCSController; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserSession; + +class PushController extends OCSController { + + /** @var IDBConnection */ + private $db; + + /** @var ISession */ + private $session; + + /** @var IUserSession */ + private $userSession; + + /** @var IProvider */ + private $tokenProvider; + + /** @var Manager */ + private $identityProof; + + /** @var Crypto */ + private $crypto; + + /** + * @param string $appName + * @param IRequest $request + * @param IDBConnection $db + * @param ISession $session + * @param IUserSession $userSession + * @param IProvider $tokenProvider + * @param Manager $identityProof + * @param Crypto $crypto + */ + public function __construct($appName, IRequest $request, IDBConnection $db, ISession $session, IUserSession $userSession, IProvider $tokenProvider, Manager $identityProof, Crypto $crypto) { + parent::__construct($appName, $request); + + $this->db = $db; + $this->session = $session; + $this->userSession = $userSession; + $this->tokenProvider = $tokenProvider; + $this->identityProof = $identityProof; + $this->crypto = $crypto; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $pushTokenHash + * @param string $devicePublicKey + * @return JSONResponse + */ + public function registerDevice($pushTokenHash, $devicePublicKey) { + $user = $this->userSession->getUser(); + if (!$user instanceof IUser) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + if (!preg_match('/^([a-f0-9]{128})$/', $pushTokenHash)) { + return new JSONResponse(['message' => 'Invalid hashed push token'], Http::STATUS_BAD_REQUEST); + } + + if (strlen($devicePublicKey) !== 450 || + strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || + strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 426) { + return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + } + + $sessionId = $this->session->getId(); + try { + $token = $this->tokenProvider->getToken($sessionId); + } catch (InvalidTokenException $e) { + return new JSONResponse(['message' => 'Could not identify session token'], Http::STATUS_BAD_REQUEST); + } + + $key = $this->identityProof->getKey($user); + + $created = $this->savePushToken($user, $token, $devicePublicKey, $pushTokenHash); + + $encryptedData = $this->crypto->encrypt(json_encode([$user->getCloudId(), $token->getId()]), $user); + return new JSONResponse([ + 'publicKey' => $key->getPublic(), + 'deviceIdentifier' => $encryptedData['message'], + 'signature' => $encryptedData['signature'], + ], $created ? Http::STATUS_CREATED : Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $devicePublicKey + * @return JSONResponse + */ + public function removeDevice($devicePublicKey) { + $user = $this->userSession->getUser(); + if (!$user instanceof IUser) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + if (strlen($devicePublicKey) !== 450 || + strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || + strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 425) { + return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + } + + $sessionId = $this->session->getId(); + try { + $token = $this->tokenProvider->getToken($sessionId); + } catch (InvalidTokenException $e) { + return new JSONResponse(['message' => 'Could not identify session token'], Http::STATUS_BAD_REQUEST); + } + + $this->deletePushToken($user, $token, $devicePublicKey); + return new JSONResponse(); + } + + /** + * @param IUser $user + * @param IToken $token + * @param string $devicePublicKey + * @param string $pushTokenHash + * @return bool If the hash was new to the database + */ + protected function savePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + $query = $this->db->getQueryBuilder(); + $query->select('pushtokenhash') + ->from('notifications_pushtokens') + ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) + ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId()))) + ->andWhere($query->expr()->eq('devicepublickey', $query->createNamedParameter($devicePublicKey))); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if (!$row) { + return $this->insertPushToken($user, $token, $devicePublicKey, $pushTokenHash); + } else if ($row['pushtokenhash'] !== $pushTokenHash) { + return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash); + } + return false; + } + + /** + * @param IUser $user + * @param IToken $token + * @param string $devicePublicKey + * @param string $pushTokenHash + * @return bool If the entry was created + */ + protected function insertPushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + $query = $this->db->getQueryBuilder(); + $query->insert('notifications_pushtokens') + ->values([ + 'uid' => $query->createNamedParameter($user->getUID()), + 'token' => $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), + 'devicepublickey' => $query->createNamedParameter($devicePublicKey), + 'pushtokenhash' => $query->createNamedParameter($pushTokenHash), + ]); + return $query->execute() > 0; + } + + /** + * @param IUser $user + * @param IToken $token + * @param string $devicePublicKey + * @param string $pushTokenHash + * @return bool If the entry was updated + */ + protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + $query = $this->db->getQueryBuilder(); + $query->update('notifications_pushtokens') + ->set('pushtokenhash', $query->createNamedParameter($pushTokenHash)) + ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) + ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('devicepublickey', $query->createNamedParameter($devicePublicKey))); + return $query->execute() > 0; + } + + /** + * @param IUser $user + * @param IToken $token + * @param string $devicePublicKey + * @return bool If the entry was deleted + */ + protected function deletePushToken(IUser $user, IToken $token, $devicePublicKey) { + $query = $this->db->getQueryBuilder(); + $query->delete('notifications_pushtokens') + ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) + ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->eq('devicepublickey', $query->createNamedParameter($devicePublicKey))); + return $query->execute() > 0; + } +} From 2fd4a9fbeb966a9d3ce1da8bb931292ecca6bd12 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 26 Jan 2017 11:09:53 +0100 Subject: [PATCH 02/32] Store the hash to be able to compare it easily Signed-off-by: Joas Schilling --- appinfo/database.xml | 5 +++++ appinfo/info.xml | 2 +- lib/Controller/PushController.php | 28 +++++++++++++++++++++++----- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/appinfo/database.xml b/appinfo/database.xml index c9bdab860..88f6c0799 100755 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -138,6 +138,11 @@ devicepublickey + clob + true + + + devicepublickeyhash text true 128 diff --git a/appinfo/info.xml b/appinfo/info.xml index 47223b535..72cde2fce 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -15,7 +15,7 @@ AGPL Joas Schilling - 1.2.0 + 1.2.1 diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 2e4f4ad7c..d7be1a014 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -110,7 +110,11 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { $key = $this->identityProof->getKey($user); - $created = $this->savePushToken($user, $token, $devicePublicKey, $pushTokenHash); + try { + $created = $this->savePushToken($user, $token, $devicePublicKey, $pushTokenHash); + } catch (\BadMethodCallException $e) { + return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + } $encryptedData = $this->crypto->encrypt(json_encode([$user->getCloudId(), $token->getId()]), $user); return new JSONResponse([ @@ -135,7 +139,7 @@ public function removeDevice($devicePublicKey) { if (strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || - strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 425) { + strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 426) { return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); } @@ -156,6 +160,7 @@ public function removeDevice($devicePublicKey) { * @param string $devicePublicKey * @param string $pushTokenHash * @return bool If the hash was new to the database + * @throws \BadMethodCallException */ protected function savePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { $query = $this->db->getQueryBuilder(); @@ -184,12 +189,15 @@ protected function savePushToken(IUser $user, IToken $token, $devicePublicKey, $ * @return bool If the entry was created */ protected function insertPushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + $devicePublicKeyHash = hash('sha512', $devicePublicKey); + $query = $this->db->getQueryBuilder(); $query->insert('notifications_pushtokens') ->values([ 'uid' => $query->createNamedParameter($user->getUID()), 'token' => $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), 'devicepublickey' => $query->createNamedParameter($devicePublicKey), + 'devicepublickeyhash' => $query->createNamedParameter($devicePublicKeyHash), 'pushtokenhash' => $query->createNamedParameter($pushTokenHash), ]); return $query->execute() > 0; @@ -201,15 +209,23 @@ protected function insertPushToken(IUser $user, IToken $token, $devicePublicKey, * @param string $devicePublicKey * @param string $pushTokenHash * @return bool If the entry was updated + * @throws \BadMethodCallException */ protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + $devicePublicKeyHash = hash('sha512', $devicePublicKey); + $query = $this->db->getQueryBuilder(); $query->update('notifications_pushtokens') ->set('pushtokenhash', $query->createNamedParameter($pushTokenHash)) ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('devicepublickey', $query->createNamedParameter($devicePublicKey))); - return $query->execute() > 0; + ->andWhere($query->expr()->eq('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))); + + if ($query->execute() !== 0) { + throw new \BadMethodCallException(); + } + + return true; } /** @@ -219,11 +235,13 @@ protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, * @return bool If the entry was deleted */ protected function deletePushToken(IUser $user, IToken $token, $devicePublicKey) { + $devicePublicKeyHash = hash('sha512', $devicePublicKey); + $query = $this->db->getQueryBuilder(); $query->delete('notifications_pushtokens') ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('devicepublickey', $query->createNamedParameter($devicePublicKey))); + ->andWhere($query->expr()->eq('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))); return $query->execute() > 0; } } From ebaa6d1a24088162d6b30e93fee9847cba1cfb97 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 26 Jan 2017 11:10:35 +0100 Subject: [PATCH 03/32] Get the token id from the session Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index d7be1a014..ce8cd7377 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -101,9 +101,9 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); } - $sessionId = $this->session->getId(); + $tokenId = $this->session->get('token-id'); try { - $token = $this->tokenProvider->getToken($sessionId); + $token = $this->tokenProvider->getTokenById($tokenId); } catch (InvalidTokenException $e) { return new JSONResponse(['message' => 'Could not identify session token'], Http::STATUS_BAD_REQUEST); } From 08ea5ffff4485478a85b363c8eb4ced987481c6b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 26 Jan 2017 11:36:30 +0100 Subject: [PATCH 04/32] Error when on deletion the device public key is wrong as well Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index ce8cd7377..68675cf81 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -150,7 +150,12 @@ public function removeDevice($devicePublicKey) { return new JSONResponse(['message' => 'Could not identify session token'], Http::STATUS_BAD_REQUEST); } - $this->deletePushToken($user, $token, $devicePublicKey); + try { + $this->deletePushToken($user, $token, $devicePublicKey); + } catch (\BadMethodCallException $e) { + return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + } + return new JSONResponse(); } @@ -233,6 +238,7 @@ protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, * @param IToken $token * @param string $devicePublicKey * @return bool If the entry was deleted + * @throws \BadMethodCallException */ protected function deletePushToken(IUser $user, IToken $token, $devicePublicKey) { $devicePublicKeyHash = hash('sha512', $devicePublicKey); @@ -242,6 +248,11 @@ protected function deletePushToken(IUser $user, IToken $token, $devicePublicKey) ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))); - return $query->execute() > 0; + + if ($query->execute() !== 0) { + throw new \BadMethodCallException(); + } + + return true; } } From b4cc0aada05a8cf95e4927949fcd90d27ced2bc0 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 26 Jan 2017 12:34:18 +0100 Subject: [PATCH 05/32] encode the signature before sending Signed-off-by: Joas Schilling --- appinfo/routes.php | 4 ++-- lib/Controller/PushController.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index a79e4c8d3..4f16200a0 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -24,7 +24,7 @@ ['name' => 'Endpoint#listNotifications', 'url' => '/api/{apiVersion}/notifications', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)']], ['name' => 'Endpoint#getNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']], ['name' => 'Endpoint#deleteNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']], - ['name' => 'Push#registerDevice', 'url' => '/api/3/push', 'verb' => 'POST'], - ['name' => 'Push#removeDevice', 'url' => '/api/3/push', 'verb' => 'DELETE'], + ['name' => 'Push#registerDevice', 'url' => '/api/v3/push', 'verb' => 'POST'], + ['name' => 'Push#removeDevice', 'url' => '/api/v3/push', 'verb' => 'DELETE'], ], ]; diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 68675cf81..43098e42c 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -116,11 +116,11 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); } - $encryptedData = $this->crypto->encrypt(json_encode([$user->getCloudId(), $token->getId()]), $user); + $encryptedData = $this->crypto->encrypt(sha1(json_encode([$user->getCloudId(), $token->getId()])), $user); return new JSONResponse([ 'publicKey' => $key->getPublic(), 'deviceIdentifier' => $encryptedData['message'], - 'signature' => $encryptedData['signature'], + 'signature' => base64_encode($encryptedData['signature']), ], $created ? Http::STATUS_CREATED : Http::STATUS_OK); } From 1fd8c357b730d912513630dc10b2e4085172fc6b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 26 Jan 2017 17:43:07 +0100 Subject: [PATCH 06/32] Fix return codes and messages Signed-off-by: Joas Schilling --- appinfo/database.xml | 3 ++- lib/Controller/PushController.php | 32 ++++++++++++++----------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/appinfo/database.xml b/appinfo/database.xml index 88f6c0799..be9a75ca9 100755 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -138,8 +138,9 @@ devicepublickey - clob + text true + 512 devicepublickeyhash diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 43098e42c..b9b105452 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -24,7 +24,6 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; -use OC\Security\IdentityProof\Crypto; use OC\Security\IdentityProof\Manager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -53,9 +52,6 @@ class PushController extends OCSController { /** @var Manager */ private $identityProof; - /** @var Crypto */ - private $crypto; - /** * @param string $appName * @param IRequest $request @@ -64,9 +60,8 @@ class PushController extends OCSController { * @param IUserSession $userSession * @param IProvider $tokenProvider * @param Manager $identityProof - * @param Crypto $crypto */ - public function __construct($appName, IRequest $request, IDBConnection $db, ISession $session, IUserSession $userSession, IProvider $tokenProvider, Manager $identityProof, Crypto $crypto) { + public function __construct($appName, IRequest $request, IDBConnection $db, ISession $session, IUserSession $userSession, IProvider $tokenProvider, Manager $identityProof) { parent::__construct($appName, $request); $this->db = $db; @@ -74,7 +69,6 @@ public function __construct($appName, IRequest $request, IDBConnection $db, ISes $this->userSession = $userSession; $this->tokenProvider = $tokenProvider; $this->identityProof = $identityProof; - $this->crypto = $crypto; } /** @@ -92,20 +86,20 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { } if (!preg_match('/^([a-f0-9]{128})$/', $pushTokenHash)) { - return new JSONResponse(['message' => 'Invalid hashed push token'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST); } if (strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 426) { - return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } $tokenId = $this->session->get('token-id'); try { $token = $this->tokenProvider->getTokenById($tokenId); } catch (InvalidTokenException $e) { - return new JSONResponse(['message' => 'Could not identify session token'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); } $key = $this->identityProof->getKey($user); @@ -113,14 +107,16 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { try { $created = $this->savePushToken($user, $token, $devicePublicKey, $pushTokenHash); } catch (\BadMethodCallException $e) { - return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } - $encryptedData = $this->crypto->encrypt(sha1(json_encode([$user->getCloudId(), $token->getId()])), $user); + $deviceIdentifier = hash('sha512', json_encode([$user->getCloudId(), $token->getId()])); + openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512); + return new JSONResponse([ 'publicKey' => $key->getPublic(), - 'deviceIdentifier' => $encryptedData['message'], - 'signature' => base64_encode($encryptedData['signature']), + 'deviceIdentifier' => $deviceIdentifier, + 'signature' => base64_encode($signature), ], $created ? Http::STATUS_CREATED : Http::STATUS_OK); } @@ -140,23 +136,23 @@ public function removeDevice($devicePublicKey) { if (strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 426) { - return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } $sessionId = $this->session->getId(); try { $token = $this->tokenProvider->getToken($sessionId); } catch (InvalidTokenException $e) { - return new JSONResponse(['message' => 'Could not identify session token'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); } try { $this->deletePushToken($user, $token, $devicePublicKey); } catch (\BadMethodCallException $e) { - return new JSONResponse(['message' => 'Invalid device public key'], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } - return new JSONResponse(); + return new JSONResponse([], Http::STATUS_ACCEPTED); } /** From ed7fc81fd8740d228730a7a6da1296834f136501 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 26 Jan 2017 17:46:59 +0100 Subject: [PATCH 07/32] Add documentation for devices Signed-off-by: Joas Schilling --- docs/push-v3.md | 186 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 docs/push-v3.md diff --git a/docs/push-v3.md b/docs/push-v3.md new file mode 100644 index 000000000..0aff0587f --- /dev/null +++ b/docs/push-v3.md @@ -0,0 +1,186 @@ +# Push notifications as a Nextcloud client device + + + +## Checking the capabilities of the Nextcloud server + +In order to find out if notifications support push on the server you can run a request against the capabilities endpoint: `/ocs/v2.php/cloud/capabilities` + +```json +{ + "ocs": { + ... + "data": { + ... + "capabilities": { + ... + "notifications": { + "ocs-endpoints": [ + ... + "push" + ] + } + } + } + } +} +``` + + + +## Subscribing at the Nextcloud server + +1. **Only on first registration on the server** The device generates a `rsa2048` key pair (`devicePrivateKey` and `devicePublicKey`). + +2. The device generates the `PushToken` for *Apple Push Notification Service* (iOS) or *Firebase Cloud Messaging* (Android) + +3. The device generates a `sha512` hash of the `PushToken` (`PushTokenHash`) + +4. The device then sends the `devicePublicKey` and `PushTokenHash` to the Nextcloud server: + + ```json + POST /ocs/v2.php/apps/notifications/api/v3/push + + { + "pushTokenHash": "{{PushTokenHash}}", + "devicePublicKey": "{{devicePublicKey}}" + } + ``` + + ​ + +### Response + +The server replies with the following status codes: + +| Status code | Meaning | +| ----------- | ---------------------------------------- | +| 200 | No further action by the device required | +| 201 | Push token was created/updated and **needs to be sent to the `Proxy`** | +| 400 | Invalid public key, device does not use a token to authenticate or the push token hash is invalid formatted | +| 401 | Device is not logged in | + + + +#### Body in case of success + +In case of `200` and `201` the reply has more information in the body: + +| Key | Type | | +| ---------------- | ------------ | ---------------------------------------- | +| publicKey | string (512) | rsa2048 public key of the user account on the instance | +| deviceIdentifier | string (128) | unique identifier encrypted with the users private key | +| signature | string (512) | base64 encoded signature of the deviceIdentifier | + + + +#### Body in case of an error + +In case of `400` the following `message` can appear in the body: + +| Error | Description | +| ------------------------ | ---------------------------------------- | +| `INVALID_PUSHTOKEN_HASH` | The hash of the push token was not a valid `sha512` hash. | +| `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. Check whether a password was used to login. | +| `INVALID_DEVICE_KEY` | The device key does not match the one registered to the provided session token. | + + + +## Unsubcribing at the Nextcloud server + +When an account is removed from a device, the device should unregister on the server. Otherwise the server sends unnecessary push notifications and might be blocked because of spam. + + + +The device should then send the `devicePublicKey` and `PushTokenHash` to the Nextcloud server: + +```json +DELETE /ocs/v2.php/apps/notifications/api/v3/push + +{ + "devicePublicKey": "{{devicePublicKey}}" +} +``` + + + +### Response + +The server replies with the following status codes: + +| Status code | Meaning | +| ----------- | ---------------------------------------- | +| 202 | Push token was deleted and **needs to be deleted from the `Proxy`** | +| 400 | Invalid public key or device does not use a token to authenticate | +| 401 | Device is not logged in | + + + +#### Body in case of an error + +In case of `400` the following `message` can appear in the body: + +| Error | Description | +| ----------------------- | ---------------------------------------- | +| `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. | +| `INVALID_DEVICE_KEY` | The device key does not match the one registered to the provided session token. | + + + +## Subscribing at the Push Proxy + +The device sends the`PushToken` as well as the `deviceIdentifier`, `signature` and the user´s `publicKey` (from the server´s response) to the Push Proxy: + +```json +POST /devices + +{ + "pushToken": "{{PushToken}}", + "deviceIdentifier": "{{deviceIdentifier}}", + "deviceIdentifierSignature": "{{signature}}", + "userPublicKey": "{{userPublicKey}}" +} +``` + + + +### Response + +The server replies with the following status codes: + +| Status code | Meaning | +| ----------- | ---------------------------------------- | +| 200 | Push token was written to the databse | +| 400 | Push token, public key or device identifier is malformed, the signature does not match | +| 403 | Device is not allowed to write the push token of the device identifier | +| 409 | In case of a conflict the device can retry with the additional field `cloudId` with the value `{{userid}}@{{serverurl}}` which allows the proxy to verify the public key and device identifier belongs to the given user on the instance | + + + +## Unsubscribing at the Push Proxy + +The device sends the `deviceIdentifier` and the user´s `publicKey` (from the server´s response) to the Push Proxy: + +```json +DELETE /devices + +{ + "deviceIdentifier": "{{deviceIdentifier}}", + "userPublicKey": "{{userPublicKey}}" +} +``` + + + +### Response + +The server replies with the following status codes: + +| Status code | Meaning | +| ----------- | ---------------------------------------- | +| 200 | Push token was deleted from the databse | +| 400 | Public key or device identifier is malformed | +| 403 | Device identifier and device public key didn't match or could not be found | + + + From 0557efa226093214c329101d712f49d5e3433ff8 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 1 Feb 2017 16:52:10 +0100 Subject: [PATCH 08/32] Add the signature to the remove request as well Signed-off-by: Joas Schilling --- docs/push-v3.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/push-v3.md b/docs/push-v3.md index 0aff0587f..dea6be339 100644 --- a/docs/push-v3.md +++ b/docs/push-v3.md @@ -159,13 +159,14 @@ The server replies with the following status codes: ## Unsubscribing at the Push Proxy -The device sends the `deviceIdentifier` and the user´s `publicKey` (from the server´s response) to the Push Proxy: +The device sends the `deviceIdentifier`, `deviceIdentifierSignature` and the user´s `publicKey` (from the server´s response) to the Push Proxy: ```json DELETE /devices { "deviceIdentifier": "{{deviceIdentifier}}", + "deviceIdentifierSignature": "{{signature}}", "userPublicKey": "{{userPublicKey}}" } ``` From 4a0943dd8d6805a5f567e129feae356ba78b50b7 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 10 Feb 2017 11:06:01 +0100 Subject: [PATCH 09/32] PHP already hashes the message itself To stay compatible with the Golang implementation here we return raw bytes and also don't hash twice. Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index b9b105452..199c2987b 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -110,12 +110,12 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } - $deviceIdentifier = hash('sha512', json_encode([$user->getCloudId(), $token->getId()])); + $deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]); openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512); return new JSONResponse([ 'publicKey' => $key->getPublic(), - 'deviceIdentifier' => $deviceIdentifier, + 'deviceIdentifier' => base64_encode(hash('sha512', $deviceIdentifier, true)), 'signature' => base64_encode($signature), ], $created ? Http::STATUS_CREATED : Http::STATUS_OK); } From 6e1e4effd36edd2bb88cfa2ff596a95a7df7f60e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 1 Mar 2017 16:50:15 +0100 Subject: [PATCH 10/32] Start the actual pushing Signed-off-by: Joas Schilling --- lib/App.php | 10 +++- lib/Push.php | 138 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 lib/Push.php diff --git a/lib/App.php b/lib/App.php index 6cb0a8a59..ccc2e4f0d 100644 --- a/lib/App.php +++ b/lib/App.php @@ -28,9 +28,12 @@ class App implements IApp { /** @var Handler */ protected $handler; + /** @var Push */ + protected $push; - public function __construct(Handler $handler) { + public function __construct(Handler $handler, Push $push) { $this->handler = $handler; + $this->push = $push; } /** @@ -39,7 +42,10 @@ public function __construct(Handler $handler) { * @since 8.2.0 */ public function notify(INotification $notification) { - $this->handler->add($notification); + $notificationId = $this->handler->add($notification); + + $notificationToPush = $this->handler->getById($notificationId, $notification->getUser()); + $this->push->pushToDevice($notificationToPush); } /** diff --git a/lib/Push.php b/lib/Push.php new file mode 100644 index 000000000..057f40968 --- /dev/null +++ b/lib/Push.php @@ -0,0 +1,138 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Notifications; + + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IProvider; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; +use OCP\Notification\IManager; +use OCP\Notification\INotification; + +class Push { + /** @var IDBConnection */ + protected $connection; + /** @var IManager */ + protected $manager; + /** @var IConfig */ + protected $config; + /** @var IProvider */ + protected $tokenProvider; + /** @var IClientService */ + protected $clientService; + /** @var ILogger */ + protected $log; + + public function __construct(IDBConnection $connection, IManager $manager, IConfig $config, IProvider $tokenProvider, IClientService $clientService, ILogger $log) { + $this->connection = $connection; + $this->manager = $manager; + $this->config = $config; + $this->tokenProvider = $tokenProvider; + $this->clientService = $clientService; + $this->log = $log; + } + + /** + * @param INotification $notification + */ + public function pushToDevice(INotification $notification) { + $devices = $this->getDevicesForUser($notification->getUser()); + + if (empty($devices)) { + return; + } + + $language = $this->config->getUserValue($notification->getUser(), 'core', 'lang', 'en'); + try { + $notification = $this->manager->prepare($notification, $language); + } catch (\InvalidArgumentException $e) { + return; + } + + $subject = $notification->getParsedSubject(); + + $collection = []; + foreach ($devices as $device) { + try { + $collection[] = $this->encryptAndSign($device, $subject); + } catch (InvalidTokenException $e) { + // Token does not exist anymore, should drop the push device entry + // FIXME delete push token + } + } + + $payload = json_encode($collection); + + $this->log->alert($payload); // TODO TEMP + + $client = $this->clientService->newClient(); + try { + $response = $client->post('http://127.0.0.1:3306', [ + 'body' => $payload, + ]); + } catch (\Exception $e) { + $this->log->logException($e, [ + 'app' => 'notifications', + ]); + return; + } + } + + /** + * @param array $device + * @param $subject + * @return array + * @throws InvalidTokenException + */ + protected function encryptAndSign(array $device, $subject) { + // Check if the token is still valid... + $this->tokenProvider->getTokenById($device['token']); + + $encryptedSubject = json_encode($subject); // FIXME use $device['devicepublickey'] + $signature = hash('sha512', $encryptedSubject); // FIXME use $userPrivateKey + return [ + 'pushTokenHash' => $device['pushtokenhash'], + 'subject' => $encryptedSubject, + 'signature' => $signature, + ]; + } + + /** + * @param string $uid + * @return array[] + */ + protected function getDevicesForUser($uid) { + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('notifications_pushtokens') + ->where($query->expr()->eq('uid', $uid)); + + $result = $query->execute(); + $devices = $result->fetchAll(); + $result->closeCursor(); + + return $devices; + } +} From af272b726b9b16b8fcdbd178097c95bd43415d71 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 2 Mar 2017 13:25:12 +0100 Subject: [PATCH 11/32] Sign and encrypt the subject Signed-off-by: Joas Schilling --- lib/Push.php | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/lib/Push.php b/lib/Push.php index 057f40968..146c1e067 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -24,10 +24,14 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; +use OC\Security\IdentityProof\Key; +use OC\Security\IdentityProof\Manager; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; use OCP\Notification\IManager; use OCP\Notification\INotification; @@ -40,16 +44,22 @@ class Push { protected $config; /** @var IProvider */ protected $tokenProvider; + /** @var Manager */ + private $keyManager; + /** @var IUserManager */ + private $userManager; /** @var IClientService */ protected $clientService; /** @var ILogger */ protected $log; - public function __construct(IDBConnection $connection, IManager $manager, IConfig $config, IProvider $tokenProvider, IClientService $clientService, ILogger $log) { + public function __construct(IDBConnection $connection, IManager $manager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) { $this->connection = $connection; $this->manager = $manager; $this->config = $config; $this->tokenProvider = $tokenProvider; + $this->keyManager = $keyManager; + $this->userManager = $userManager; $this->clientService = $clientService; $this->log = $log; } @@ -59,8 +69,9 @@ public function __construct(IDBConnection $connection, IManager $manager, IConfi */ public function pushToDevice(INotification $notification) { $devices = $this->getDevicesForUser($notification->getUser()); + $user = $this->userManager->get($notification->getUser()); - if (empty($devices)) { + if (empty($devices) || !($user instanceof IUser)) { return; } @@ -71,15 +82,18 @@ public function pushToDevice(INotification $notification) { return; } - $subject = $notification->getParsedSubject(); + $userKey = $this->keyManager->getKey($user); $collection = []; foreach ($devices as $device) { try { - $collection[] = $this->encryptAndSign($device, $subject); + $collection[] = $this->encryptAndSign($userKey, $device, $notification); } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry // FIXME delete push token + } catch (\InvalidArgumentException $e) { + // Token does not exist anymore, should drop the push device entry + // FIXME delete push token } } @@ -101,17 +115,29 @@ public function pushToDevice(INotification $notification) { } /** + * @param Key $userKey * @param array $device - * @param $subject + * @param INotification $notification * @return array * @throws InvalidTokenException + * @throws \InvalidArgumentException */ - protected function encryptAndSign(array $device, $subject) { + protected function encryptAndSign(Key $userKey, array $device, INotification $notification) { // Check if the token is still valid... $this->tokenProvider->getTokenById($device['token']); - $encryptedSubject = json_encode($subject); // FIXME use $device['devicepublickey'] - $signature = hash('sha512', $encryptedSubject); // FIXME use $userPrivateKey + $data = [ + 'app' => $notification->getApp(), + 'subject' => $notification->getParsedSubject(), + ]; + + if (!openssl_private_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) { + $this->log->error(openssl_error_string(), ['app' => 'notifications']); + throw new \InvalidArgumentException('Failed to encrypt message for device'); + } + + openssl_sign(json_encode($encryptedSubject), $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512); + return [ 'pushTokenHash' => $device['pushtokenhash'], 'subject' => $encryptedSubject, From a6b1645f3a83a99c47249c761c874a5f806e5645 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 3 Mar 2017 13:41:58 +0100 Subject: [PATCH 12/32] Encode signatures and encrypted messages and fix query and encryption Signed-off-by: Joas Schilling --- lib/Push.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Push.php b/lib/Push.php index 146c1e067..44d8718a3 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -84,10 +84,10 @@ public function pushToDevice(INotification $notification) { $userKey = $this->keyManager->getKey($user); - $collection = []; + $pushNotifications = []; foreach ($devices as $device) { try { - $collection[] = $this->encryptAndSign($userKey, $device, $notification); + $pushNotifications[] = $this->encryptAndSign($userKey, $device, $notification); } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry // FIXME delete push token @@ -97,14 +97,14 @@ public function pushToDevice(INotification $notification) { } } - $payload = json_encode($collection); + $payload = json_encode($pushNotifications); $this->log->alert($payload); // TODO TEMP $client = $this->clientService->newClient(); try { $response = $client->post('http://127.0.0.1:3306', [ - 'body' => $payload, + 'body' => $pushNotifications, ]); } catch (\Exception $e) { $this->log->logException($e, [ @@ -131,17 +131,19 @@ protected function encryptAndSign(Key $userKey, array $device, INotification $no 'subject' => $notification->getParsedSubject(), ]; - if (!openssl_private_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) { + if (!openssl_public_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) { $this->log->error(openssl_error_string(), ['app' => 'notifications']); throw new \InvalidArgumentException('Failed to encrypt message for device'); } openssl_sign(json_encode($encryptedSubject), $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512); + $base64EncryptedSubject = base64_encode($encryptedSubject); + $base64Signature = base64_encode($signature); return [ 'pushTokenHash' => $device['pushtokenhash'], - 'subject' => $encryptedSubject, - 'signature' => $signature, + 'subject' => $base64EncryptedSubject, + 'signature' => $base64Signature, ]; } @@ -153,7 +155,7 @@ protected function getDevicesForUser($uid) { $query = $this->connection->getQueryBuilder(); $query->select('*') ->from('notifications_pushtokens') - ->where($query->expr()->eq('uid', $uid)); + ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))); $result = $query->execute(); $devices = $result->fetchAll(); From 362ed9b0d71e85371fbd6952650633a3a212d704 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 3 Mar 2017 14:05:10 +0100 Subject: [PATCH 13/32] Add docs Signed-off-by: Joas Schilling --- docs/push-v3.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/push-v3.md b/docs/push-v3.md index dea6be339..b43033715 100644 --- a/docs/push-v3.md +++ b/docs/push-v3.md @@ -179,9 +179,35 @@ The server replies with the following status codes: | Status code | Meaning | | ----------- | ---------------------------------------- | -| 200 | Push token was deleted from the databse | +| 200 | Push token was deleted from the database | | 400 | Public key or device identifier is malformed | | 403 | Device identifier and device public key didn't match or could not be found | +## Pushed notifications + +The pushed notifications format depends on the service that is used. +In case of Apple Push Notification Service, the payload of a push notification looks like the following: + +```json +{ + "aps" : { + "alert" : { + "loc-key" : "NEW_NOTIFICATION" + } + }, + "subject" : "*Encrypted subject*", + "signature" : "*Signature*" +} +``` + +| Attribute | Meaning | +| ----------- | ---------------------------------------- | +| `aps` | As defined by the [apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW1). Since this message can not be encrypted, it contains **no sensitive** content. | +| `subject` | The subject is encrypted with the device´s *public key*. | +| `signature` | The signature is a sha512 signature over the encrypted subject using the user´s private key. | + +### Verification +So a device should verify the signature using the user´s public key. +If the signature is okay, the subject can be decrypted using the device´s private key. From 8d2ced526503255197174c91735b39e1b7cfc003 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 6 Mar 2017 15:36:54 +0100 Subject: [PATCH 14/32] Add some more help on debugging and fix server URL Signed-off-by: Joas Schilling --- lib/Push.php | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/Push.php b/lib/Push.php index 44d8718a3..86387a140 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -26,6 +26,7 @@ use OC\Authentication\Token\IProvider; use OC\Security\IdentityProof\Key; use OC\Security\IdentityProof\Manager; +use OCP\AppFramework\Http; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; @@ -97,13 +98,10 @@ public function pushToDevice(INotification $notification) { } } - $payload = json_encode($pushNotifications); - - $this->log->alert($payload); // TODO TEMP - $client = $this->clientService->newClient(); try { - $response = $client->post('http://127.0.0.1:3306', [ + $pushServer = rtrim($this->config->getAppValue('notifications', 'push_server', 'https://push-notifications.nextcloud.com'), '/'); + $response = $client->post($pushServer . '/notifications', [ 'body' => $pushNotifications, ]); } catch (\Exception $e) { @@ -112,6 +110,21 @@ public function pushToDevice(INotification $notification) { ]); return; } + + $status = $response->getStatusCode(); + if ($status !== Http::STATUS_OK && $status !== Http::STATUS_SERVICE_UNAVAILABLE) { + $body = $response->getBody(); + $this->log->error('Could not send notification to push server: {error}',[ + 'error' => is_string($body) ? $body : 'no reason given', + 'app' => 'notifications', + ]); + } else if ($status === Http::STATUS_SERVICE_UNAVAILABLE && $this->config->getSystemValue('debug', false)) { + $body = $response->getBody(); + $this->log->debug('Could not send notification to push server: {error}',[ + 'error' => is_string($body) ? $body : 'no reason given', + 'app' => 'notifications', + ]); + } } /** From 682d276da87b076754d281eb13f81f7efffa7e9b Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 9 Mar 2017 11:03:03 +0100 Subject: [PATCH 15/32] Also store the device identifier Signed-off-by: Joas Schilling --- appinfo/database.xml | 6 ++++++ appinfo/info.xml | 2 +- lib/Controller/PushController.php | 20 ++++++++++++-------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/appinfo/database.xml b/appinfo/database.xml index be9a75ca9..9d1027957 100755 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -136,6 +136,12 @@ true 4 + + deviceidentifier + text + true + 128 + devicepublickey text diff --git a/appinfo/info.xml b/appinfo/info.xml index 72cde2fce..277efd47f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -15,7 +15,7 @@ AGPL Joas Schilling - 1.2.1 + 1.2.2 diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 199c2987b..96515235c 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -104,18 +104,19 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { $key = $this->identityProof->getKey($user); + $deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]); + openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512); + $deviceIdentifier = base64_encode(hash('sha512', $deviceIdentifier, true)); + try { - $created = $this->savePushToken($user, $token, $devicePublicKey, $pushTokenHash); + $created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash); } catch (\BadMethodCallException $e) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } - $deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]); - openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512); - return new JSONResponse([ 'publicKey' => $key->getPublic(), - 'deviceIdentifier' => base64_encode(hash('sha512', $deviceIdentifier, true)), + 'deviceIdentifier' => $deviceIdentifier, 'signature' => base64_encode($signature), ], $created ? Http::STATUS_CREATED : Http::STATUS_OK); } @@ -158,12 +159,13 @@ public function removeDevice($devicePublicKey) { /** * @param IUser $user * @param IToken $token + * @param string $deviceIdentifier * @param string $devicePublicKey * @param string $pushTokenHash * @return bool If the hash was new to the database * @throws \BadMethodCallException */ - protected function savePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + protected function savePushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash) { $query = $this->db->getQueryBuilder(); $query->select('pushtokenhash') ->from('notifications_pushtokens') @@ -175,7 +177,7 @@ protected function savePushToken(IUser $user, IToken $token, $devicePublicKey, $ $result->closeCursor(); if (!$row) { - return $this->insertPushToken($user, $token, $devicePublicKey, $pushTokenHash); + return $this->insertPushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash); } else if ($row['pushtokenhash'] !== $pushTokenHash) { return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash); } @@ -185,11 +187,12 @@ protected function savePushToken(IUser $user, IToken $token, $devicePublicKey, $ /** * @param IUser $user * @param IToken $token + * @param string $deviceIdentifier * @param string $devicePublicKey * @param string $pushTokenHash * @return bool If the entry was created */ - protected function insertPushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash) { $devicePublicKeyHash = hash('sha512', $devicePublicKey); $query = $this->db->getQueryBuilder(); @@ -197,6 +200,7 @@ protected function insertPushToken(IUser $user, IToken $token, $devicePublicKey, ->values([ 'uid' => $query->createNamedParameter($user->getUID()), 'token' => $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), + 'deviceidentifier' => $query->createNamedParameter($deviceIdentifier), 'devicepublickey' => $query->createNamedParameter($devicePublicKey), 'devicepublickeyhash' => $query->createNamedParameter($devicePublicKeyHash), 'pushtokenhash' => $query->createNamedParameter($pushTokenHash), From 3e53f38630e1729af8b6fd6c3c951330f8d444fd Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 9 Mar 2017 11:03:42 +0100 Subject: [PATCH 16/32] Fix signing and make the sending okay for golang Signed-off-by: Joas Schilling --- lib/Push.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/Push.php b/lib/Push.php index 86387a140..fb21b268d 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -88,7 +88,7 @@ public function pushToDevice(INotification $notification) { $pushNotifications = []; foreach ($devices as $device) { try { - $pushNotifications[] = $this->encryptAndSign($userKey, $device, $notification); + $pushNotifications[] = json_encode($this->encryptAndSign($userKey, $device, $notification)); } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry // FIXME delete push token @@ -102,7 +102,9 @@ public function pushToDevice(INotification $notification) { try { $pushServer = rtrim($this->config->getAppValue('notifications', 'push_server', 'https://push-notifications.nextcloud.com'), '/'); $response = $client->post($pushServer . '/notifications', [ - 'body' => $pushNotifications, + 'body' => [ + 'notifications' => $pushNotifications, + ], ]); } catch (\Exception $e) { $this->log->logException($e, [ @@ -149,11 +151,12 @@ protected function encryptAndSign(Key $userKey, array $device, INotification $no throw new \InvalidArgumentException('Failed to encrypt message for device'); } - openssl_sign(json_encode($encryptedSubject), $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512); - $base64EncryptedSubject = base64_encode($encryptedSubject); + openssl_sign($encryptedSubject, $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512); + $base64EncryptedSubject = base64_encode(hash('sha512', $encryptedSubject, true)); $base64Signature = base64_encode($signature); return [ + 'deviceIdentifier' => $device['deviceidentifier'], 'pushTokenHash' => $device['pushtokenhash'], 'subject' => $base64EncryptedSubject, 'signature' => $base64Signature, From 62605403a42fed2401f58c776b4f7dd746494839 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 29 Mar 2017 11:43:23 +0200 Subject: [PATCH 17/32] Receive the proxy server from the app This allows users to use one client against multiple different servers Signed-off-by: Joas Schilling --- appinfo/database.xml | 6 +++ docs/push-v3.md | 27 ++++++----- lib/Controller/PushController.php | 81 ++++++++++++------------------- lib/Push.php | 60 ++++++++++++----------- 4 files changed, 84 insertions(+), 90 deletions(-) diff --git a/appinfo/database.xml b/appinfo/database.xml index 9d1027957..07febf9d2 100755 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -160,6 +160,12 @@ true 128 + + proxyserver + text + true + 256 + oc_notifpushtoken diff --git a/docs/push-v3.md b/docs/push-v3.md index b43033715..9b20eb8dd 100644 --- a/docs/push-v3.md +++ b/docs/push-v3.md @@ -6,7 +6,7 @@ In order to find out if notifications support push on the server you can run a request against the capabilities endpoint: `/ocs/v2.php/cloud/capabilities` -```json +``` { "ocs": { ... @@ -36,14 +36,15 @@ In order to find out if notifications support push on the server you can run a r 3. The device generates a `sha512` hash of the `PushToken` (`PushTokenHash`) -4. The device then sends the `devicePublicKey` and `PushTokenHash` to the Nextcloud server: +4. The device then sends the `devicePublicKey`, `PushTokenHash` and `proxyServerUrl` to the Nextcloud server: - ```json + ``` POST /ocs/v2.php/apps/notifications/api/v3/push { "pushTokenHash": "{{PushTokenHash}}", - "devicePublicKey": "{{devicePublicKey}}" + "devicePublicKey": "{{devicePublicKey}}", + "proxyServer": "{{proxyServerUrl}}" } ``` @@ -83,6 +84,7 @@ In case of `400` the following `message` can appear in the body: | `INVALID_PUSHTOKEN_HASH` | The hash of the push token was not a valid `sha512` hash. | | `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. Check whether a password was used to login. | | `INVALID_DEVICE_KEY` | The device key does not match the one registered to the provided session token. | +| `INVALID_PROXY_SERVER` | The proxy server was not a valid https URL. | @@ -92,14 +94,10 @@ When an account is removed from a device, the device should unregister on the se -The device should then send the `devicePublicKey` and `PushTokenHash` to the Nextcloud server: +The device should then send a `DELETE` request to the Nextcloud server: -```json +``` DELETE /ocs/v2.php/apps/notifications/api/v3/push - -{ - "devicePublicKey": "{{devicePublicKey}}" -} ``` @@ -110,6 +108,7 @@ The server replies with the following status codes: | Status code | Meaning | | ----------- | ---------------------------------------- | +| 200 | Push token was not registered on the server | | 202 | Push token was deleted and **needs to be deleted from the `Proxy`** | | 400 | Invalid public key or device does not use a token to authenticate | | 401 | Device is not logged in | @@ -131,7 +130,7 @@ In case of `400` the following `message` can appear in the body: The device sends the`PushToken` as well as the `deviceIdentifier`, `signature` and the user´s `publicKey` (from the server´s response) to the Push Proxy: -```json +``` POST /devices { @@ -161,7 +160,7 @@ The server replies with the following status codes: The device sends the `deviceIdentifier`, `deviceIdentifierSignature` and the user´s `publicKey` (from the server´s response) to the Push Proxy: -```json +``` DELETE /devices { @@ -190,7 +189,9 @@ The server replies with the following status codes: The pushed notifications format depends on the service that is used. In case of Apple Push Notification Service, the payload of a push notification looks like the following: -```json +***⚠️ Warning:** Maybe different since we switched to FCM instead of APNS* + +``` { "aps" : { "alert" : { diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 96515235c..626b431ff 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -77,9 +77,10 @@ public function __construct($appName, IRequest $request, IDBConnection $db, ISes * * @param string $pushTokenHash * @param string $devicePublicKey + * @param string $proxyServer * @return JSONResponse */ - public function registerDevice($pushTokenHash, $devicePublicKey) { + public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { $user = $this->userSession->getUser(); if (!$user instanceof IUser) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); @@ -95,6 +96,10 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } + if (!filter_input(FILTER_VALIDATE_URL, $proxyServer) || strpos($proxyServer, 'https://') !== 0 || strlen($proxyServer)> 256) { + return new JSONResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST); + } + $tokenId = $this->session->get('token-id'); try { $token = $this->tokenProvider->getTokenById($tokenId); @@ -108,11 +113,7 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512); $deviceIdentifier = base64_encode(hash('sha512', $deviceIdentifier, true)); - try { - $created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash); - } catch (\BadMethodCallException $e) { - return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); - } + $created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer); return new JSONResponse([ 'publicKey' => $key->getPublic(), @@ -125,21 +126,14 @@ public function registerDevice($pushTokenHash, $devicePublicKey) { * @NoAdminRequired * @NoCSRFRequired * - * @param string $devicePublicKey * @return JSONResponse */ - public function removeDevice($devicePublicKey) { + public function removeDevice() { $user = $this->userSession->getUser(); if (!$user instanceof IUser) { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - if (strlen($devicePublicKey) !== 450 || - strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || - strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 426) { - return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); - } - $sessionId = $this->session->getId(); try { $token = $this->tokenProvider->getToken($sessionId); @@ -147,13 +141,11 @@ public function removeDevice($devicePublicKey) { return new JSONResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); } - try { - $this->deletePushToken($user, $token, $devicePublicKey); - } catch (\BadMethodCallException $e) { - return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); + if ($this->deletePushToken($user, $token)) { + return new JSONResponse([], Http::STATUS_ACCEPTED); } - return new JSONResponse([], Http::STATUS_ACCEPTED); + return new JSONResponse([], Http::STATUS_OK); } /** @@ -162,26 +154,24 @@ public function removeDevice($devicePublicKey) { * @param string $deviceIdentifier * @param string $devicePublicKey * @param string $pushTokenHash + * @param string $proxyServer * @return bool If the hash was new to the database - * @throws \BadMethodCallException */ - protected function savePushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash) { + protected function savePushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer) { $query = $this->db->getQueryBuilder(); - $query->select('pushtokenhash') + $query->select('*') ->from('notifications_pushtokens') ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) - ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId()))) - ->andWhere($query->expr()->eq('devicepublickey', $query->createNamedParameter($devicePublicKey))); + ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId()))); $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); if (!$row) { - return $this->insertPushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash); - } else if ($row['pushtokenhash'] !== $pushTokenHash) { - return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash); + return $this->insertPushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer); } - return false; + + return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash, $proxyServer); } /** @@ -190,9 +180,10 @@ protected function savePushToken(IUser $user, IToken $token, $deviceIdentifier, * @param string $deviceIdentifier * @param string $devicePublicKey * @param string $pushTokenHash + * @param string $proxyServer * @return bool If the entry was created */ - protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash) { + protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer) { $devicePublicKeyHash = hash('sha512', $devicePublicKey); $query = $this->db->getQueryBuilder(); @@ -204,6 +195,7 @@ protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier 'devicepublickey' => $query->createNamedParameter($devicePublicKey), 'devicepublickeyhash' => $query->createNamedParameter($devicePublicKeyHash), 'pushtokenhash' => $query->createNamedParameter($pushTokenHash), + 'proxyserver' => $query->createNamedParameter($proxyServer), ]); return $query->execute() > 0; } @@ -213,46 +205,35 @@ protected function insertPushToken(IUser $user, IToken $token, $deviceIdentifier * @param IToken $token * @param string $devicePublicKey * @param string $pushTokenHash + * @param string $proxyServer * @return bool If the entry was updated - * @throws \BadMethodCallException */ - protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash) { + protected function updatePushToken(IUser $user, IToken $token, $devicePublicKey, $pushTokenHash, $proxyServer) { $devicePublicKeyHash = hash('sha512', $devicePublicKey); $query = $this->db->getQueryBuilder(); $query->update('notifications_pushtokens') + ->set('devicepublickey', $query->createNamedParameter($devicePublicKey)) + ->set('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash)) ->set('pushtokenhash', $query->createNamedParameter($pushTokenHash)) + ->set('proxyserver', $query->createNamedParameter($proxyServer)) ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) - ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))); - - if ($query->execute() !== 0) { - throw new \BadMethodCallException(); - } + ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))); - return true; + return $query->execute() !== 0; } /** * @param IUser $user * @param IToken $token - * @param string $devicePublicKey * @return bool If the entry was deleted - * @throws \BadMethodCallException */ - protected function deletePushToken(IUser $user, IToken $token, $devicePublicKey) { - $devicePublicKeyHash = hash('sha512', $devicePublicKey); - + protected function deletePushToken(IUser $user, IToken $token) { $query = $this->db->getQueryBuilder(); $query->delete('notifications_pushtokens') ->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID()))) - ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))) - ->andWhere($query->expr()->eq('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))); - - if ($query->execute() !== 0) { - throw new \BadMethodCallException(); - } + ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT))); - return true; + return $query->execute() !== 0; } } diff --git a/lib/Push.php b/lib/Push.php index fb21b268d..f9f7d8dc0 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -88,7 +88,10 @@ public function pushToDevice(INotification $notification) { $pushNotifications = []; foreach ($devices as $device) { try { - $pushNotifications[] = json_encode($this->encryptAndSign($userKey, $device, $notification)); + if (!isset($pushNotifications[$device['proxyserver']])) { + $pushNotifications[$device['proxyserver']] = []; + } + $pushNotifications[$device['proxyserver']] = json_encode($this->encryptAndSign($userKey, $device, $notification)); } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry // FIXME delete push token @@ -99,33 +102,36 @@ public function pushToDevice(INotification $notification) { } $client = $this->clientService->newClient(); - try { - $pushServer = rtrim($this->config->getAppValue('notifications', 'push_server', 'https://push-notifications.nextcloud.com'), '/'); - $response = $client->post($pushServer . '/notifications', [ - 'body' => [ - 'notifications' => $pushNotifications, - ], - ]); - } catch (\Exception $e) { - $this->log->logException($e, [ - 'app' => 'notifications', - ]); - return; - } + foreach ($pushNotifications as $proxyServer => $notifications) { + try { + $response = $client->post(rtrim($proxyServer, '/') . '/notifications', [ + 'body' => [ + 'notifications' => $notifications, + ], + ]); + } catch (\Exception $e) { + $this->log->logException($e, [ + 'app' => 'notifications', + ]); + return; + } - $status = $response->getStatusCode(); - if ($status !== Http::STATUS_OK && $status !== Http::STATUS_SERVICE_UNAVAILABLE) { - $body = $response->getBody(); - $this->log->error('Could not send notification to push server: {error}',[ - 'error' => is_string($body) ? $body : 'no reason given', - 'app' => 'notifications', - ]); - } else if ($status === Http::STATUS_SERVICE_UNAVAILABLE && $this->config->getSystemValue('debug', false)) { - $body = $response->getBody(); - $this->log->debug('Could not send notification to push server: {error}',[ - 'error' => is_string($body) ? $body : 'no reason given', - 'app' => 'notifications', - ]); + $status = $response->getStatusCode(); + if ($status !== Http::STATUS_OK && $status !== Http::STATUS_SERVICE_UNAVAILABLE) { + $body = $response->getBody(); + $this->log->error('Could not send notification to push server [{url}]: {error}',[ + 'error' => is_string($body) ? $body : 'no reason given', + 'url' => $proxyServer, + 'app' => 'notifications', + ]); + } else if ($status === Http::STATUS_SERVICE_UNAVAILABLE && $this->config->getSystemValue('debug', false)) { + $body = $response->getBody(); + $this->log->debug('Could not send notification to push server [{url}]: {error}',[ + 'error' => is_string($body) ? $body : 'no reason given', + 'url' => $proxyServer, + 'app' => 'notifications', + ]); + } } } From 468017b8454be3165f1ab020cda6ab9bbcb918ea Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 29 Mar 2017 13:06:24 +0200 Subject: [PATCH 18/32] Fix wrong function name Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 626b431ff..36aef017c 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -96,7 +96,7 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } - if (!filter_input(FILTER_VALIDATE_URL, $proxyServer) || strpos($proxyServer, 'https://') !== 0 || strlen($proxyServer)> 256) { + if (!filter_var($proxyServer, FILTER_VALIDATE_URL) || strpos($proxyServer, 'https://') !== 0 || strlen($proxyServer)> 256) { return new JSONResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST); } From bd64d6a19dcab9e48fb59247694371fd74547f98 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 5 Apr 2017 12:48:15 +0200 Subject: [PATCH 19/32] Allow localhost via http Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 36aef017c..273975448 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -96,7 +96,15 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } - if (!filter_var($proxyServer, FILTER_VALIDATE_URL) || strpos($proxyServer, 'https://') !== 0 || strlen($proxyServer)> 256) { + if ( + !filter_var($proxyServer, FILTER_VALIDATE_URL) || + strlen($proxyServer) > 256 || + ( + strpos($proxyServer, 'https://') !== 0 && + strpos($proxyServer, 'http://localhost:') !== 0 && + strpos($proxyServer, 'http://localhost/') !== 0 + ) + ) { return new JSONResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST); } From 9a634448fce54f31d2d17093745145b1b6bc145f Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 5 Apr 2017 12:48:49 +0200 Subject: [PATCH 20/32] NC12 can do this Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 273975448..c80e679a6 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -73,7 +73,6 @@ public function __construct($appName, IRequest $request, IDBConnection $db, ISes /** * @NoAdminRequired - * @NoCSRFRequired * * @param string $pushTokenHash * @param string $devicePublicKey @@ -132,7 +131,6 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { /** * @NoAdminRequired - * @NoCSRFRequired * * @return JSONResponse */ From 185751a5a53f16089be38d63a02b9c3c033344cb Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 7 Apr 2017 11:39:38 +0200 Subject: [PATCH 21/32] Clarify docs Signed-off-by: Joas Schilling --- docs/push-v3.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/docs/push-v3.md b/docs/push-v3.md index 9b20eb8dd..4d55313ca 100644 --- a/docs/push-v3.md +++ b/docs/push-v3.md @@ -58,7 +58,7 @@ The server replies with the following status codes: | ----------- | ---------------------------------------- | | 200 | No further action by the device required | | 201 | Push token was created/updated and **needs to be sent to the `Proxy`** | -| 400 | Invalid public key, device does not use a token to authenticate or the push token hash is invalid formatted | +| 400 | Invalid device public key; device does not use a token to authenticate; the push token hash is invalid formatted; the proxy server URL is invalid; | | 401 | Device is not logged in | @@ -110,7 +110,7 @@ The server replies with the following status codes: | ----------- | ---------------------------------------- | | 200 | Push token was not registered on the server | | 202 | Push token was deleted and **needs to be deleted from the `Proxy`** | -| 400 | Invalid public key or device does not use a token to authenticate | +| 400 | Device does not use a token to authenticate | | 401 | Device is not logged in | @@ -122,7 +122,6 @@ In case of `400` the following `message` can appear in the body: | Error | Description | | ----------------------- | ---------------------------------------- | | `INVALID_SESSION_TOKEN` | The authentication token of the request could not be identified. | -| `INVALID_DEVICE_KEY` | The device key does not match the one registered to the provided session token. | @@ -186,27 +185,27 @@ The server replies with the following status codes: ## Pushed notifications -The pushed notifications format depends on the service that is used. -In case of Apple Push Notification Service, the payload of a push notification looks like the following: +The pushed notifications is defined by the [Firebase Cloud Messaging HTTP Protocol](https://firebase.google.com/docs/cloud-messaging/http-server-ref#send-downstream). The sample content of a Nextcloud push notification looks like the following: -***⚠️ Warning:** Maybe different since we switched to FCM instead of APNS* - -``` +```json { - "aps" : { - "alert" : { - "loc-key" : "NEW_NOTIFICATION" - } + "to" : "APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx...", + "notification" : { + "body" : "NEW_NOTIFICATION", + "body_loc_key" : "NEW_NOTIFICATION", + "title" : "NEW_NOTIFICATION", + "title_loc_key" : "NEW_NOTIFICATION" }, - "subject" : "*Encrypted subject*", - "signature" : "*Signature*" + "data" : { + "subject" : "*Encrypted subject*", + "signature" : "*Signature*" + } } ``` | Attribute | Meaning | | ----------- | ---------------------------------------- | -| `aps` | As defined by the [apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW1). Since this message can not be encrypted, it contains **no sensitive** content. | -| `subject` | The subject is encrypted with the device´s *public key*. | +| `subject` | The subject is encrypted with the device´s *public key*. | | `signature` | The signature is a sha512 signature over the encrypted subject using the user´s private key. | ### Verification From 7f0a9f019371f297e49955a1097ba42d9608a2e7 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Apr 2017 17:17:16 +0200 Subject: [PATCH 22/32] Fix trailing new line Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index c80e679a6..089428a25 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -89,9 +89,10 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { return new JSONResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST); } - if (strlen($devicePublicKey) !== 450 || - strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----') !== 0 || - strpos($devicePublicKey, '-----END PUBLIC KEY-----') !== 426) { + if ( + (strlen($devicePublicKey) !== 450 && strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) || + (strlen($devicePublicKey) !== 451 && strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425) || + strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----' . "\n") !== 0) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } From 475ad40e443a1be52fa05134cfabf75d1ac9a2b6 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Apr 2017 17:25:33 +0200 Subject: [PATCH 23/32] Add capability for push Signed-off-by: Joas Schilling --- docs/{push-v3.md => push-v2.md} | 4 ++-- lib/Capabilities.php | 3 +++ tests/Unit/CapabilitiesTest.php | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) rename docs/{push-v3.md => push-v2.md} (99%) diff --git a/docs/push-v3.md b/docs/push-v2.md similarity index 99% rename from docs/push-v3.md rename to docs/push-v2.md index 4d55313ca..e635c226a 100644 --- a/docs/push-v3.md +++ b/docs/push-v2.md @@ -15,9 +15,9 @@ In order to find out if notifications support push on the server you can run a r "capabilities": { ... "notifications": { - "ocs-endpoints": [ + "push": [ ... - "push" + "devices" ] } } diff --git a/lib/Capabilities.php b/lib/Capabilities.php index a6ca12caf..6b832f628 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -45,6 +45,9 @@ public function getCapabilities() { 'icons', 'rich-strings', ], + 'push' => [ + 'devices', + ], ], ]; } diff --git a/tests/Unit/CapabilitiesTest.php b/tests/Unit/CapabilitiesTest.php index 764e8de8e..802d2b9e1 100644 --- a/tests/Unit/CapabilitiesTest.php +++ b/tests/Unit/CapabilitiesTest.php @@ -38,6 +38,9 @@ public function testGetCapabilities() { 'icons', 'rich-strings', ], + 'push' => [ + 'devices', + ], ], ], $capabilities->getCapabilities()); } From 8ae1023e60ca188a12caf5b3abf51261a4332ad7 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 18 Apr 2017 17:29:59 +0200 Subject: [PATCH 24/32] Fix routes Signed-off-by: Joas Schilling --- appinfo/info.xml | 2 +- appinfo/routes.php | 4 ++-- docs/push-v2.md | 4 ++-- tests/Unit/AppInfo/RoutesTest.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 277efd47f..3873d1813 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -15,7 +15,7 @@ AGPL Joas Schilling - 1.2.2 + 2.0.0 diff --git a/appinfo/routes.php b/appinfo/routes.php index 4f16200a0..205c35391 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -24,7 +24,7 @@ ['name' => 'Endpoint#listNotifications', 'url' => '/api/{apiVersion}/notifications', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)']], ['name' => 'Endpoint#getNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'GET', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']], ['name' => 'Endpoint#deleteNotification', 'url' => '/api/{apiVersion}/notifications/{id}', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v(1|2)', 'id' => '\d+']], - ['name' => 'Push#registerDevice', 'url' => '/api/v3/push', 'verb' => 'POST'], - ['name' => 'Push#removeDevice', 'url' => '/api/v3/push', 'verb' => 'DELETE'], + ['name' => 'Push#registerDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v2']], + ['name' => 'Push#removeDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v2']], ], ]; diff --git a/docs/push-v2.md b/docs/push-v2.md index e635c226a..84aed3e4e 100644 --- a/docs/push-v2.md +++ b/docs/push-v2.md @@ -39,7 +39,7 @@ In order to find out if notifications support push on the server you can run a r 4. The device then sends the `devicePublicKey`, `PushTokenHash` and `proxyServerUrl` to the Nextcloud server: ``` - POST /ocs/v2.php/apps/notifications/api/v3/push + POST /ocs/v2.php/apps/notifications/api/v2/push { "pushTokenHash": "{{PushTokenHash}}", @@ -97,7 +97,7 @@ When an account is removed from a device, the device should unregister on the se The device should then send a `DELETE` request to the Nextcloud server: ``` -DELETE /ocs/v2.php/apps/notifications/api/v3/push +DELETE /ocs/v2.php/apps/notifications/api/v2/push ``` diff --git a/tests/Unit/AppInfo/RoutesTest.php b/tests/Unit/AppInfo/RoutesTest.php index ccac67307..4e584321a 100644 --- a/tests/Unit/AppInfo/RoutesTest.php +++ b/tests/Unit/AppInfo/RoutesTest.php @@ -37,6 +37,6 @@ public function testRoutes() { $this->assertCount(1, $routes); $this->assertArrayHasKey('ocs', $routes); $this->assertInternalType('array', $routes['ocs']); - $this->assertGreaterThanOrEqual(3, sizeof($routes['ocs'])); + $this->assertCount(5, $routes['ocs']); } } From ad83ef61d2b9a3942a18db2c0a2a99bbf0470c10 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 09:51:25 +0200 Subject: [PATCH 25/32] Wrong demorgan Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 089428a25..274fc00cd 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -90,8 +90,8 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { } if ( - (strlen($devicePublicKey) !== 450 && strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) || - (strlen($devicePublicKey) !== 451 && strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425) || + ((strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) && + (strlen($devicePublicKey) !== 451 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425)) || strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----' . "\n") !== 0) { return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } From 280e707c2a52e776f3ec410ce02a7d8d11d4dabd Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 11:00:26 +0200 Subject: [PATCH 26/32] Use DataResponse so the result is valid OCS Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 274fc00cd..1b4de7244 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -26,7 +26,7 @@ use OC\Authentication\Token\IToken; use OC\Security\IdentityProof\Manager; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -77,23 +77,23 @@ public function __construct($appName, IRequest $request, IDBConnection $db, ISes * @param string $pushTokenHash * @param string $devicePublicKey * @param string $proxyServer - * @return JSONResponse + * @return DataResponse */ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { $user = $this->userSession->getUser(); if (!$user instanceof IUser) { - return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + return new DataResponse([], Http::STATUS_UNAUTHORIZED); } if (!preg_match('/^([a-f0-9]{128})$/', $pushTokenHash)) { - return new JSONResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST); + return new DataResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST); } if ( ((strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) && (strlen($devicePublicKey) !== 451 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425)) || strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----' . "\n") !== 0) { - return new JSONResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); + return new DataResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST); } if ( @@ -105,14 +105,14 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { strpos($proxyServer, 'http://localhost/') !== 0 ) ) { - return new JSONResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST); + return new DataResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST); } $tokenId = $this->session->get('token-id'); try { $token = $this->tokenProvider->getTokenById($tokenId); } catch (InvalidTokenException $e) { - return new JSONResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); + return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); } $key = $this->identityProof->getKey($user); @@ -123,7 +123,7 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { $created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer); - return new JSONResponse([ + return new DataResponse([ 'publicKey' => $key->getPublic(), 'deviceIdentifier' => $deviceIdentifier, 'signature' => base64_encode($signature), @@ -133,26 +133,26 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { /** * @NoAdminRequired * - * @return JSONResponse + * @return DataResponse */ public function removeDevice() { $user = $this->userSession->getUser(); if (!$user instanceof IUser) { - return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + return new DataResponse([], Http::STATUS_UNAUTHORIZED); } $sessionId = $this->session->getId(); try { $token = $this->tokenProvider->getToken($sessionId); } catch (InvalidTokenException $e) { - return new JSONResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); + return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); } if ($this->deletePushToken($user, $token)) { - return new JSONResponse([], Http::STATUS_ACCEPTED); + return new DataResponse([], Http::STATUS_ACCEPTED); } - return new JSONResponse([], Http::STATUS_OK); + return new DataResponse([], Http::STATUS_OK); } /** From a2346e6a8fef52d7a466c86468e4ed4786bcad55 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 11:37:59 +0200 Subject: [PATCH 27/32] Fix existing tests Signed-off-by: Joas Schilling --- tests/Unit/AppInfo/AppTest.php | 12 +++---- tests/Unit/AppTest.php | 62 +++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/tests/Unit/AppInfo/AppTest.php b/tests/Unit/AppInfo/AppTest.php index 0b1ccea61..9e4ec8f99 100644 --- a/tests/Unit/AppInfo/AppTest.php +++ b/tests/Unit/AppInfo/AppTest.php @@ -22,6 +22,7 @@ namespace OCA\Notifications\Tests\Unit\AppInfo; +use OC\User\Session; use OCA\Notifications\App; use OCA\Notifications\Tests\Unit\TestCase; use OCP\IRequest; @@ -46,14 +47,9 @@ class AppTest extends TestCase { protected function setUp() { parent::setUp(); - $this->manager = $this->getMockBuilder(IManager::class) - ->getMock(); - - $this->request = $this->getMockBuilder(IRequest::class) - ->getMock(); - - $this->session = $this->getMockBuilder(IUserSession::class) - ->getMock(); + $this->manager = $this->createMock(IManager::class); + $this->request = $this->createMock(IRequest::class); + $this->session = $this->createMock(Session::class); $this->overwriteService('NotificationManager', $this->manager); $this->overwriteService('Request', $this->request); diff --git a/tests/Unit/AppTest.php b/tests/Unit/AppTest.php index 691a562c7..64e395f09 100644 --- a/tests/Unit/AppTest.php +++ b/tests/Unit/AppTest.php @@ -25,12 +25,14 @@ use OCA\Notifications\App; use OCA\Notifications\Handler; +use OCA\Notifications\Push; use OCP\Notification\INotification; class AppTest extends TestCase { /** @var Handler|\PHPUnit_Framework_MockObject_MockObject */ protected $handler; - + /** @var Push|\PHPUnit_Framework_MockObject_MockObject */ + protected $push; /** @var INotification|\PHPUnit_Framework_MockObject_MockObject */ protected $notification; @@ -40,34 +42,68 @@ class AppTest extends TestCase { protected function setUp() { parent::setUp(); - $this->handler = $this->getMockBuilder(Handler::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->notification = $this->getMockBuilder(INotification::class) - ->disableOriginalConstructor() - ->getMock(); + $this->handler = $this->createMock(Handler::class); + $this->push = $this->createMock(Push::class); + $this->notification = $this->createMock(INotification::class); $this->app = new App( - $this->handler + $this->handler, + $this->push ); } - public function testNotify() { + public function dataNotify() { + return [ + [23, 'user1'], + [42, 'user2'], + ]; + } + + /** + * @dataProvider dataNotify + * + * @param int $id + * @param string $user + */ + public function testNotify($id, $user) { + $this->notification->expects($this->once()) + ->method('getUser') + ->willReturn($user); + $this->handler->expects($this->once()) ->method('add') + ->with($this->notification) + ->willReturn($id); + $this->handler->expects($this->once()) + ->method('getById') + ->with($id, $user) + ->willReturn($this->notification); + $this->push->expects($this->once()) + ->method('pushToDevice') ->with($this->notification); $this->app->notify($this->notification); } - public function testGetCount() { + public function dataGetCount() { + return [ + [23], + [42], + ]; + } + + /** + * @dataProvider dataGetCount + * + * @param int $count + */ + public function testGetCount($count) { $this->handler->expects($this->once()) ->method('count') ->with($this->notification) - ->willReturn(42); + ->willReturn($count); - $this->assertSame(42, $this->app->getCount($this->notification)); + $this->assertSame($count, $this->app->getCount($this->notification)); } public function testMarkProcessed() { From 3a755927978c79621161bf313280942adafb3560 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 12:32:17 +0200 Subject: [PATCH 28/32] Add more tests Signed-off-by: Joas Schilling --- lib/AppInfo/Application.php | 1 - tests/Unit/AppInfo/ApplicationTest.php | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 92f97961e..af7e6259e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -33,7 +33,6 @@ public function __construct() { parent::__construct('notifications'); $container = $this->getContainer(); - $container->registerAlias('EndpointController', EndpointController::class); $container->registerCapability(Capabilities::class); // FIXME this is for automatic DI because it is not in DIContainer diff --git a/tests/Unit/AppInfo/ApplicationTest.php b/tests/Unit/AppInfo/ApplicationTest.php index 3dd659516..652de2607 100644 --- a/tests/Unit/AppInfo/ApplicationTest.php +++ b/tests/Unit/AppInfo/ApplicationTest.php @@ -26,7 +26,9 @@ use OCA\Notifications\AppInfo\Application; use OCA\Notifications\Capabilities; use OCA\Notifications\Controller\EndpointController; +use OCA\Notifications\Controller\PushController; use OCA\Notifications\Handler; +use OCA\Notifications\Push; use OCA\Notifications\Tests\Unit\TestCase; use OCP\AppFramework\IAppContainer; use OCP\AppFramework\OCSController; @@ -63,12 +65,13 @@ public function dataContainerQuery() { array(App::class, IApp::class), array(Capabilities::class), array(Handler::class), + array(Push::class), // Controller/ - array('EndpointController', EndpointController::class), - array('EndpointController', OCSController::class), array(EndpointController::class), array(EndpointController::class, OCSController::class), + array(PushController::class), + array(PushController::class, OCSController::class), ); } From 37f5a935f1dc285ab88bd3a64a5dcffe9364c441 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 13:27:18 +0200 Subject: [PATCH 29/32] Delete invalid push tokens to avoid getting banned Signed-off-by: Joas Schilling --- lib/Push.php | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/Push.php b/lib/Push.php index f9f7d8dc0..baf2c8723 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -27,6 +27,7 @@ use OC\Security\IdentityProof\Key; use OC\Security\IdentityProof\Manager; use OCP\AppFramework\Http; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\IDBConnection; @@ -38,7 +39,7 @@ class Push { /** @var IDBConnection */ - protected $connection; + protected $db; /** @var IManager */ protected $manager; /** @var IConfig */ @@ -55,7 +56,7 @@ class Push { protected $log; public function __construct(IDBConnection $connection, IManager $manager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) { - $this->connection = $connection; + $this->db = $connection; $this->manager = $manager; $this->config = $config; $this->tokenProvider = $tokenProvider; @@ -94,10 +95,10 @@ public function pushToDevice(INotification $notification) { $pushNotifications[$device['proxyserver']] = json_encode($this->encryptAndSign($userKey, $device, $notification)); } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry - // FIXME delete push token + $this->deletePushToken($device['token']); } catch (\InvalidArgumentException $e) { - // Token does not exist anymore, should drop the push device entry - // FIXME delete push token + // Failed to encrypt message for device: public key is invalid + $this->deletePushToken($device['token']); } } @@ -135,6 +136,18 @@ public function pushToDevice(INotification $notification) { } } + /** + * @param int $tokenId + * @return bool + */ + protected function deletePushToken($tokenId) { + $query = $this->db->getQueryBuilder(); + $query->delete('notifications_pushtokens') + ->where($query->expr()->eq('token', $query->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT))); + + return $query->execute() !== 0; + } + /** * @param Key $userKey * @param array $device @@ -174,7 +187,7 @@ protected function encryptAndSign(Key $userKey, array $device, INotification $no * @return array[] */ protected function getDevicesForUser($uid) { - $query = $this->connection->getQueryBuilder(); + $query = $this->db->getQueryBuilder(); $query->select('*') ->from('notifications_pushtokens') ->where($query->expr()->eq('uid', $query->createNamedParameter($uid))); From c4625ff19a4ef5f242713f0fe7817aa04bdd930d Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 13:51:03 +0200 Subject: [PATCH 30/32] Allow sending multiple notifications via the same proxy Signed-off-by: Joas Schilling --- lib/Push.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Push.php b/lib/Push.php index baf2c8723..d1d547134 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -92,7 +92,7 @@ public function pushToDevice(INotification $notification) { if (!isset($pushNotifications[$device['proxyserver']])) { $pushNotifications[$device['proxyserver']] = []; } - $pushNotifications[$device['proxyserver']] = json_encode($this->encryptAndSign($userKey, $device, $notification)); + $pushNotifications[$device['proxyserver']][] = json_encode($this->encryptAndSign($userKey, $device, $notification)); } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry $this->deletePushToken($device['token']); From 6a4c8af6f8fc9578f3ac10b5f700114ad8a63f78 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 19 Apr 2017 15:24:06 +0200 Subject: [PATCH 31/32] Add unit tests for Push Signed-off-by: Joas Schilling --- lib/Push.php | 60 ++--- tests/Unit/PushTest.php | 487 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 522 insertions(+), 25 deletions(-) create mode 100644 tests/Unit/PushTest.php diff --git a/lib/Push.php b/lib/Push.php index d1d547134..5859e256b 100644 --- a/lib/Push.php +++ b/lib/Push.php @@ -34,14 +34,14 @@ use OCP\ILogger; use OCP\IUser; use OCP\IUserManager; -use OCP\Notification\IManager; +use OCP\Notification\IManager as INotificationManager; use OCP\Notification\INotification; class Push { /** @var IDBConnection */ protected $db; - /** @var IManager */ - protected $manager; + /** @var INotificationManager */ + protected $notificationManager; /** @var IConfig */ protected $config; /** @var IProvider */ @@ -55,9 +55,9 @@ class Push { /** @var ILogger */ protected $log; - public function __construct(IDBConnection $connection, IManager $manager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) { + public function __construct(IDBConnection $connection, INotificationManager $notificationManager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) { $this->db = $connection; - $this->manager = $manager; + $this->notificationManager = $notificationManager; $this->config = $config; $this->tokenProvider = $tokenProvider; $this->keyManager = $keyManager; @@ -70,16 +70,19 @@ public function __construct(IDBConnection $connection, IManager $manager, IConfi * @param INotification $notification */ public function pushToDevice(INotification $notification) { - $devices = $this->getDevicesForUser($notification->getUser()); $user = $this->userManager->get($notification->getUser()); + if (!($user instanceof IUser)) { + return; + } - if (empty($devices) || !($user instanceof IUser)) { + $devices = $this->getDevicesForUser($notification->getUser()); + if (empty($devices)) { return; } $language = $this->config->getUserValue($notification->getUser(), 'core', 'lang', 'en'); try { - $notification = $this->manager->prepare($notification, $language); + $notification = $this->notificationManager->prepare($notification, $language); } catch (\InvalidArgumentException $e) { return; } @@ -89,10 +92,13 @@ public function pushToDevice(INotification $notification) { $pushNotifications = []; foreach ($devices as $device) { try { - if (!isset($pushNotifications[$device['proxyserver']])) { - $pushNotifications[$device['proxyserver']] = []; + $payload = json_encode($this->encryptAndSign($userKey, $device, $notification)); + + $proxyServer = rtrim($device['proxyserver'], '/'); + if (!isset($pushNotifications[$proxyServer])) { + $pushNotifications[$proxyServer] = []; } - $pushNotifications[$device['proxyserver']][] = json_encode($this->encryptAndSign($userKey, $device, $notification)); + $pushNotifications[$proxyServer][] = $payload; } catch (InvalidTokenException $e) { // Token does not exist anymore, should drop the push device entry $this->deletePushToken($device['token']); @@ -102,10 +108,14 @@ public function pushToDevice(INotification $notification) { } } + if (empty($pushNotifications)) { + return; + } + $client = $this->clientService->newClient(); foreach ($pushNotifications as $proxyServer => $notifications) { try { - $response = $client->post(rtrim($proxyServer, '/') . '/notifications', [ + $response = $client->post($proxyServer . '/notifications', [ 'body' => [ 'notifications' => $notifications, ], @@ -114,7 +124,7 @@ public function pushToDevice(INotification $notification) { $this->log->logException($e, [ 'app' => 'notifications', ]); - return; + continue; } $status = $response->getStatusCode(); @@ -136,18 +146,6 @@ public function pushToDevice(INotification $notification) { } } - /** - * @param int $tokenId - * @return bool - */ - protected function deletePushToken($tokenId) { - $query = $this->db->getQueryBuilder(); - $query->delete('notifications_pushtokens') - ->where($query->expr()->eq('token', $query->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT))); - - return $query->execute() !== 0; - } - /** * @param Key $userKey * @param array $device @@ -198,4 +196,16 @@ protected function getDevicesForUser($uid) { return $devices; } + + /** + * @param int $tokenId + * @return bool + */ + protected function deletePushToken($tokenId) { + $query = $this->db->getQueryBuilder(); + $query->delete('notifications_pushtokens') + ->where($query->expr()->eq('token', $query->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT))); + + return $query->execute() !== 0; + } } diff --git a/tests/Unit/PushTest.php b/tests/Unit/PushTest.php new file mode 100644 index 000000000..6399b375b --- /dev/null +++ b/tests/Unit/PushTest.php @@ -0,0 +1,487 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Notifications\Tests\Unit; + + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IProvider; +use OC\Security\IdentityProof\Key; +use OC\Security\IdentityProof\Manager; +use OCA\Notifications\Push; +use OCP\AppFramework\Http; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\IConfig; +use OCP\Http\Client\IClientService; +use OCP\IDBConnection; +use OCP\ILogger; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; + +/** + * Class PushTest + * + * @package OCA\Notifications\Tests\Unit + * @group DB + */ +class PushTest extends TestCase { + /** @var IDBConnection */ + protected $db; + /** @var INotificationManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $notificationManager; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + protected $config; + /** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */ + protected $tokenProvider; + /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ + protected $keyManager; + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + protected $userManager; + /** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */ + protected $clientService; + /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ + protected $logger; + + protected function setUp() { + parent::setUp(); + + $this->db = \OC::$server->getDatabaseConnection(); + $this->notificationManager = $this->createMock(INotificationManager::class); + $this->config = $this->createMock(IConfig::class); + $this->tokenProvider = $this->createMock(IProvider::class); + $this->keyManager = $this->createMock(Manager::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->clientService = $this->createMock(IClientService::class); + $this->logger = $this->createMock(ILogger::class); + } + + /** + * @param string[] $methods + * @return Push|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getPush(array $methods = []) { + if (!empty($methods)) { + return $this->getMockBuilder(Push::class) + ->setConstructorArgs([ + $this->db, + $this->notificationManager, + $this->config, + $this->tokenProvider, + $this->keyManager, + $this->userManager, + $this->clientService, + $this->logger, + ]) + ->setMethods($methods) + ->getMock(); + } + + return new Push( + $this->db, + $this->notificationManager, + $this->config, + $this->tokenProvider, + $this->keyManager, + $this->userManager, + $this->clientService, + $this->logger + ); + } + + public function testPushToDeviceInvalidUser() { + $push = $this->getPush(); + $this->keyManager->expects($this->never()) + ->method('getKey'); + $this->clientService->expects($this->never()) + ->method('newClient'); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->once()) + ->method('getUser') + ->willReturn('invalid'); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('invalid') + ->willReturn(null); + + $push->pushToDevice($notification); + } + + public function testPushToDeviceNoDevices() { + $push = $this->getPush(['getDevicesForUser']); + $this->keyManager->expects($this->never()) + ->method('getKey'); + $this->clientService->expects($this->never()) + ->method('newClient'); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->exactly(2)) + ->method('getUser') + ->willReturn('valid'); + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('valid') + ->willReturn($user); + + $push->expects($this->once()) + ->method('getDevicesForUser') + ->willReturn([]); + + $push->pushToDevice($notification); + } + + public function testPushToDeviceNotPrepared() { + $push = $this->getPush(['getDevicesForUser']); + $this->keyManager->expects($this->never()) + ->method('getKey'); + $this->clientService->expects($this->never()) + ->method('newClient'); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->exactly(3)) + ->method('getUser') + ->willReturn('valid'); + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('valid') + ->willReturn($user); + + $push->expects($this->once()) + ->method('getDevicesForUser') + ->willReturn([[ + 'proxyserver' => 'proxyserver1', + 'token' => 'token1', + ]]); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('valid', 'core', 'lang', 'en') + ->willReturn('de'); + + $this->notificationManager->expects($this->once()) + ->method('prepare') + ->with($notification, 'de') + ->willThrowException(new \InvalidArgumentException()); + + $push->pushToDevice($notification); + } + + public function testPushToDeviceInvalidToken() { + $push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']); + $this->clientService->expects($this->never()) + ->method('newClient'); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->exactly(3)) + ->method('getUser') + ->willReturn('valid'); + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('valid') + ->willReturn($user); + + $push->expects($this->once()) + ->method('getDevicesForUser') + ->willReturn([[ + 'proxyserver' => 'proxyserver1', + 'token' => 23, + ]]); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('valid', 'core', 'lang', 'en') + ->willReturn('ru'); + + $this->notificationManager->expects($this->once()) + ->method('prepare') + ->with($notification, 'ru') + ->willReturnArgument(0); + + + /** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */ + $key = $this->createMock(Key::class); + + $this->keyManager->expects($this->once()) + ->method('getKey') + ->with($user) + ->willReturn($key); + + $push->expects($this->once()) + ->method('encryptAndSign') + ->willThrowException(new InvalidTokenException()); + + $push->expects($this->once()) + ->method('deletePushToken') + ->with(23); + + $push->pushToDevice($notification); + } + + public function testPushToDeviceEncryptionError() { + $push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']); + $this->clientService->expects($this->never()) + ->method('newClient'); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->exactly(3)) + ->method('getUser') + ->willReturn('valid'); + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('valid') + ->willReturn($user); + + $push->expects($this->once()) + ->method('getDevicesForUser') + ->willReturn([[ + 'proxyserver' => 'proxyserver1', + 'token' => 23, + ]]); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('valid', 'core', 'lang', 'en') + ->willReturn('ru'); + + $this->notificationManager->expects($this->once()) + ->method('prepare') + ->with($notification, 'ru') + ->willReturnArgument(0); + + + /** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */ + $key = $this->createMock(Key::class); + + $this->keyManager->expects($this->once()) + ->method('getKey') + ->with($user) + ->willReturn($key); + + $push->expects($this->once()) + ->method('encryptAndSign') + ->willThrowException(new \InvalidArgumentException()); + + $push->expects($this->once()) + ->method('deletePushToken') + ->with(23); + + $push->pushToDevice($notification); + } + + public function dataPushToDeviceSending() { + return [ + [true], + [false], + ]; + } + + /** + * @dataProvider dataPushToDeviceSending + * @param bool $isDebug + */ + public function testPushToDeviceSending($isDebug) { + $push = $this->getPush(['getDevicesForUser', 'encryptAndSign', 'deletePushToken']); + + /** @var INotification|\PHPUnit_Framework_MockObject_MockObject $notification */ + $notification = $this->createMock(INotification::class); + $notification->expects($this->exactly(3)) + ->method('getUser') + ->willReturn('valid'); + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->createMock(IUser::class); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('valid') + ->willReturn($user); + + $push->expects($this->once()) + ->method('getDevicesForUser') + ->willReturn([ + [ + 'proxyserver' => 'proxyserver1', + 'token' => 16, + ], + [ + 'proxyserver' => 'proxyserver1/', + 'token' => 23, + ], + [ + 'proxyserver' => 'badrequest', + 'token' => 42, + ], + [ + 'proxyserver' => 'unavailable', + 'token' => 48, + ], + [ + 'proxyserver' => 'ok', + 'token' => 64, + ], + ]); + + $this->config->expects($this->once()) + ->method('getUserValue') + ->with('valid', 'core', 'lang', 'en') + ->willReturn('ru'); + + $this->notificationManager->expects($this->once()) + ->method('prepare') + ->with($notification, 'ru') + ->willReturnArgument(0); + + /** @var Key|\PHPUnit_Framework_MockObject_MockObject $key */ + $key = $this->createMock(Key::class); + + $this->keyManager->expects($this->once()) + ->method('getKey') + ->with($user) + ->willReturn($key); + + $push->expects($this->exactly(5)) + ->method('encryptAndSign') + ->willReturn(['Payload']); + + $push->expects($this->never()) + ->method('deletePushToken'); + + /** @var IClient|\PHPUnit_Framework_MockObject_MockObject $client */ + $client = $this->createMock(IClient::class); + + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $e = new \Exception(); + $client->expects($this->at(0)) + ->method('post') + ->with('proxyserver1/notifications', [ + 'body' => [ + 'notifications' => ['["Payload"]', '["Payload"]'], + ], + ]) + ->willThrowException($e); + + $this->logger->expects($this->at(0)) + ->method('logException') + ->with($e, [ + 'app' => 'notifications', + ]); + + /** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */ + $response1 = $this->createMock(IResponse::class); + $response1->expects($this->once()) + ->method('getStatusCode') + ->willReturn(Http::STATUS_BAD_REQUEST); + $response1->expects($this->once()) + ->method('getBody') + ->willReturn(null); + $client->expects($this->at(1)) + ->method('post') + ->with('badrequest/notifications', [ + 'body' => [ + 'notifications' => ['["Payload"]'], + ], + ]) + ->willReturn($response1); + + $this->logger->expects($this->at(1)) + ->method('error') + ->with('Could not send notification to push server [{url}]: {error}', [ + 'error' => 'no reason given', + 'url' => 'badrequest', + 'app' => 'notifications', + ]); + + /** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */ + $response2 = $this->createMock(IResponse::class); + $response2->expects($this->once()) + ->method('getStatusCode') + ->willReturn(Http::STATUS_SERVICE_UNAVAILABLE); + $response2->expects($isDebug ? $this->once() : $this->never()) + ->method('getBody') + ->willReturn('Maintenance'); + $client->expects($this->at(2)) + ->method('post') + ->with('unavailable/notifications', [ + 'body' => [ + 'notifications' => ['["Payload"]'], + ], + ]) + ->willReturn($response2); + + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('debug', false) + ->willReturn($isDebug); + + $this->logger->expects($isDebug ? $this->at(2) : $this->never()) + ->method('debug') + ->with('Could not send notification to push server [{url}]: {error}', [ + 'error' => 'Maintenance', + 'url' => 'unavailable', + 'app' => 'notifications', + ]); + + /** @var IResponse|\PHPUnit_Framework_MockObject_MockObject $response1 */ + $response3 = $this->createMock(IResponse::class); + $response3->expects($this->once()) + ->method('getStatusCode') + ->willReturn(Http::STATUS_OK); + $client->expects($this->at(3)) + ->method('post') + ->with('ok/notifications', [ + 'body' => [ + 'notifications' => ['["Payload"]'], + ], + ]) + ->willReturn($response3); + + $push->pushToDevice($notification); + } +} From edbb99192eba9fe8d4630214b7aff1d35a3066a0 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 21 Apr 2017 11:33:49 +0200 Subject: [PATCH 32/32] Add unit tests for the PushController Signed-off-by: Joas Schilling --- lib/Controller/PushController.php | 10 +- tests/Unit/Controller/PushControllerTest.php | 508 +++++++++++++++++++ 2 files changed, 511 insertions(+), 7 deletions(-) create mode 100644 tests/Unit/Controller/PushControllerTest.php diff --git a/lib/Controller/PushController.php b/lib/Controller/PushController.php index 1b4de7244..aef6eb3d7 100644 --- a/lib/Controller/PushController.php +++ b/lib/Controller/PushController.php @@ -99,11 +99,7 @@ public function registerDevice($pushTokenHash, $devicePublicKey, $proxyServer) { if ( !filter_var($proxyServer, FILTER_VALIDATE_URL) || strlen($proxyServer) > 256 || - ( - strpos($proxyServer, 'https://') !== 0 && - strpos($proxyServer, 'http://localhost:') !== 0 && - strpos($proxyServer, 'http://localhost/') !== 0 - ) + !preg_match('/^(https\:\/\/|http\:\/\/localhost(\:[0-9]{0,5})?\/)/', $proxyServer) ) { return new DataResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST); } @@ -141,9 +137,9 @@ public function removeDevice() { return new DataResponse([], Http::STATUS_UNAUTHORIZED); } - $sessionId = $this->session->getId(); + $tokenId = $this->session->get('token-id'); try { - $token = $this->tokenProvider->getToken($sessionId); + $token = $this->tokenProvider->getTokenById($tokenId); } catch (InvalidTokenException $e) { return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST); } diff --git a/tests/Unit/Controller/PushControllerTest.php b/tests/Unit/Controller/PushControllerTest.php new file mode 100644 index 000000000..2d2401dc1 --- /dev/null +++ b/tests/Unit/Controller/PushControllerTest.php @@ -0,0 +1,508 @@ + + * @author Thomas Müller + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Notifications\Tests\Unit\Controller; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OC\Security\IdentityProof\Key; +use OC\Security\IdentityProof\Manager; +use OCA\Notifications\Controller\PushController; +use OCA\Notifications\Tests\Unit\TestCase; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IDBConnection; +use OCP\IRequest; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserSession; + +class PushControllerTest extends TestCase { + /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ + protected $request; + /** @var IDBConnection|\PHPUnit_Framework_MockObject_MockObject */ + protected $db; + /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */ + protected $session; + /** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */ + protected $userSession; + /** @var IProvider|\PHPUnit_Framework_MockObject_MockObject */ + protected $tokenProvider; + /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ + protected $identityProof; + + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject */ + protected $user; + /** @var PushController */ + protected $controller; + + protected $devicePublicKey = '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Or1KumSDfk8dT0MuCW9 +WS5wkVOpNsbz2OIJFBYrBvu6joC2iQo9StONMaXoTQj5Ucak9UBtC60PHyTkIDFb +HOpCST5onmIAtZdqHN/3ABOBeHVU/notdRIl/menGM64jiqGWvE06F1+yZ8GGcGQ +8RKzabqMd2K1iUohXP625uzTABVaiwz3u8nGEwui5R6Pf5Fy6DccuqdUMtJIfW21 +Z4Tj48Tw+pR+fUrGpa1Wg+wiwlg7ISK8Symml1Rd6hSRXK2t8Opm/kjH9ZX8oVwn +RSO1ehjzRpTY+gdw/5gvwMZI0XmrIanZmZHwePRR4HC6FLPrL2OQG3gWikDIPyTS +hQIDAQAB +-----END PUBLIC KEY-----'; + + protected $userPrivateKey = '-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDPR0uV6e1cNSoy +vsITBvGyYpOIn9vI7zpEhk7FGGwdOTd2dxxJ2ikegRJ6Fr2Ojce15K3zfiasXPen +TAQuFEXecGoP9WY+DS5X1LfCpj9EeAOBfVGKeQDst5z/GoXeU+YqWbayJTp6vFRj +7o5X6QDCCXy25Kt4snNDWTHPlMc44BLjZ6w+Wj0D2ySlz1dGpunc0vwYN/uEyjr9 +ztmiN82TZtZHgzN43DJSv7tLufsZgGsWnVlytXmsi4QuCAKcm92X2ZtIXkn5niMW +DxJJepqFx7pC3ILXMZKYolAtt91VvLiGQjzURhq7HA4QdqvFyKXp0uLN2rKZjqQ0 +2nUzC34XAgMBAAECggEAFrL/Ew7IIKXt1hrP1BeZlmh3MaoX/pw8LE7tB2aSSG0A +pueKYIgUorON23LsFVVvfnrpldXF1HBl6ptHhehQcnirFM5SAQ+eeJ3h9d4Q5aWi +9KZNrLVtpX7CIam86UkU1qR2fnHXQqOnNj5ktjndDGLPlpPaN2CLgN+etdXcL10g +G5fltrFnTzYgkYap/eNkY+ivA+0xqc1l3jP2i5PHihv1adcoiOuam36GARM9C51X +fyWvMtxMvkRAZsdTATtRcQsEoJuQ3Rvseei38forkQdRn9p61UW8VT6Wa/+DWebO +Ll4OAv1RH4H2V6nrYY2ILJNnPzP8V4hjP9OGEAUQ8QKBgQDssSBUmb8Ztt6SsHNr +fgnbJBGAYizB1oAr6W1kLTQCq+BYirSYWMcJ/rakx+VCPmZ1fbbGYjPX5yVUsskx +jQ/GUT7D8lMIQNZiI9CqWR0+fJpVJ/zxwrPT2jqu8lEJxq2i/WB0nRHCgosGBTmw +UqhRGLkE5Ds14Q0zePZbdpAAyQKBgQDgL+yftcJEam8c3ipkrv02aT7vghoB0pAg +JNSSwhXED1CTboccY4daOfTYdt/PnkVmndENrUGMRyEbAY0DDK6hclG6/gE3fwn4 +mL33IIzQ9BCoXxr3tcS0r4iQjbGKorUNJW1OwmkqyMZ4POF9BSkLXpTTcJaM5WxU +8JU9PmLX3wKBgFNpuLMX27j8MUQQ2xwuttp7w48zCgLlzRWsldiP9ZxbZhzOBQcL +glmLYmJ/79OAmisduqP/R7X2x7kpqK3FwKFrUGtNouVttB+x73+ZGC1FTD5mcUXi +D+3BIp002EpRsi+Wi7+M+w1JZCUjAkmZV6f8xndq11MNlNFm96sUBXvBAoGAJ9hc +tgYYARDprrfN0RdI6eLKzMbS2IAUHaJuJadZNv+B0rJSUTlfVSn32oFGRiBbNWHX +RhcFD2mU+LfN2DzozMkEvbdnf/WUUBrVqJagcILwcvx0TpJ/451PKGIGrB0/EJcW +Vmk3R+NnYvdvHElOgjbNPMdF+sTL/EzGOZxc9QECgYBNY4LAAKqrw47p+lcRi31O +X4fhdGWAIFyiUliPDkxzEl8857FbT5c6qhdes3Gyc9tSF1wh0X7lpCDquWXYLP1V +9WNvdon+YMRi9BKpO0SlE07lwFANBpz+wJkhONVJBMzvKbxEnMRPRJ4lWa0VAAGE +j2ZL3j2Nwefj3HrR/AkeFA== +-----END PRIVATE KEY----- +'; + + protected $userPublicKey = '-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz0dLlentXDUqMr7CEwbx +smKTiJ/byO86RIZOxRhsHTk3dnccSdopHoESeha9jo3HteSt834mrFz3p0wELhRF +3nBqD/VmPg0uV9S3wqY/RHgDgX1RinkA7Lec/xqF3lPmKlm2siU6erxUY+6OV+kA +wgl8tuSreLJzQ1kxz5THOOAS42esPlo9A9skpc9XRqbp3NL8GDf7hMo6/c7ZojfN +k2bWR4MzeNwyUr+7S7n7GYBrFp1ZcrV5rIuELggCnJvdl9mbSF5J+Z4jFg8SSXqa +hce6QtyC1zGSmKJQLbfdVby4hkI81EYauxwOEHarxcil6dLizdqymY6kNNp1Mwt+ +FwIDAQAB +-----END PUBLIC KEY----- +'; + + + protected function setUp() { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->db = $this->createMock(IDBConnection::class); + $this->session = $this->createMock(ISession::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->tokenProvider = $this->createMock(IProvider::class); + $this->identityProof = $this->createMock(Manager::class); + } + + protected function getController(array $methods = []) { + if (empty($methods)) { + return new PushController( + 'notifications', + $this->request, + $this->db, + $this->session, + $this->userSession, + $this->tokenProvider, + $this->identityProof + ); + } + + return $this->getMockBuilder(PushController::class) + ->setConstructorArgs([ + 'notifications', + $this->request, + $this->db, + $this->session, + $this->userSession, + $this->tokenProvider, + $this->identityProof, + ]) + ->setMethods($methods) + ->getMock(); + } + + public function dataRegisterDevice() { + return [ + 'not authenticated' => [ + '', + '', + '', + false, + 0, + false, + null, + [], + Http::STATUS_UNAUTHORIZED + ], + 'too short token hash' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e47', + '', + '', + true, + 0, + false, + null, + ['message' => 'INVALID_PUSHTOKEN_HASH'], + Http::STATUS_BAD_REQUEST, + ], + 'too long token hash' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e4722', + '', + '', + true, + 0, + false, + null, + ['message' => 'INVALID_PUSHTOKEN_HASH'], + Http::STATUS_BAD_REQUEST, + ], + 'invalid char in token hash' => [ + 'rb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + '', + '', + true, + 0, + false, + null, + ['message' => 'INVALID_PUSHTOKEN_HASH'], + Http::STATUS_BAD_REQUEST, + ], + 'device key invalid start' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + substr($this->devicePublicKey, 1), + '', + true, + 0, + false, + null, + ['message' => 'INVALID_DEVICE_KEY'], + Http::STATUS_BAD_REQUEST, + ], + 'device key invalid end' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + substr($this->devicePublicKey, 0, -1), + '', + true, + 0, + false, + null, + ['message' => 'INVALID_DEVICE_KEY'], + Http::STATUS_BAD_REQUEST, + ], + 'device key too much end' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey . "\n\n", + '', + true, + 0, + false, + null, + ['message' => 'INVALID_DEVICE_KEY'], + Http::STATUS_BAD_REQUEST, + ], + 'device key without trailing new line' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + '', + true, + 0, + false, + null, + ['message' => 'INVALID_PROXY_SERVER'], + Http::STATUS_BAD_REQUEST, + ], + 'device key with trailing new line' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey . "\n", + '', + true, + 0, + false, + null, + ['message' => 'INVALID_PROXY_SERVER'], + Http::STATUS_BAD_REQUEST, + ], + 'invalid push proxy' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + 'localhost', + true, + 0, + false, + null, + ['message' => 'INVALID_PROXY_SERVER'], + Http::STATUS_BAD_REQUEST, + ], + 'using localhost' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + 'http://localhost/', + true, + 23, + false, + null, + ['message' => 'INVALID_SESSION_TOKEN'], + Http::STATUS_BAD_REQUEST, + ], + 'using localhost with port' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + 'http://localhost:8088/', + true, + 23, + false, + null, + ['message' => 'INVALID_SESSION_TOKEN'], + Http::STATUS_BAD_REQUEST, + ], + 'using production' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + 'https://push-notifications.nextcloud.com/', + true, + 23, + false, + null, + ['message' => 'INVALID_SESSION_TOKEN'], + Http::STATUS_BAD_REQUEST, + ], + 'created or updated' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + 'https://push-notifications.nextcloud.com/', + true, + 23, + true, + true, + [ + 'publicKey' => $this->userPublicKey, + 'deviceIdentifier' => 'XUCEZ1EHvTUcVhIvrQQQ1XcP0ZD2BFdFqw4EYbOhBfiEgXgirurR4x/ve4GSSyfivvbQOdOkZUM+g4m+tSb0Ew==', + 'signature' => 'LRhbXO71WYX9qqDbQX7C+87YaaFfWoT/vG0DlaXdBz6+lhyOA0dw/1Ggz3fd7RerCQ0MfgnnTyxO+cSeRpUaPdA2yPjfoiPpfYA5SOJQGF3comS/HYna3fHiFDbOoM3BJOnjvqiSZdxA/ICdyl2mEEC5wO7AZ4OZKBTa5XfL7eSCXZLEv1YldqcLOStbXrI7voDQocTMJxoQZI/j8BVcf2i3D6F454aXIFDrYYzC2PQY+CKJoXZW0m0RMWaTM2B8tBmFFwrmaGLDqcjjpd33TsTtsV5DB7WimffLBPpOuGV4Z1Kiagp/mxpPLz2NImNV79mDX9gY3ZppCZTwChP5qQ==', + ], + Http::STATUS_CREATED, + ], + 'not updated' => [ + 'bb9b52140661ee4f2c31e02ea50a8f67ba353bffc58aa981718f90bd2aa2bd8fc08cad4c0b3ed8f7eb9d79d6a577be75d084bbeb963da1ad74d9279e0014e472', + $this->devicePublicKey, + 'https://push-notifications.nextcloud.com/', + true, + 42, + true, + false, + [ + 'publicKey' => $this->userPublicKey, + 'deviceIdentifier' => 'x9vSImcGjhzR9BfZ/XbbUqqCCNC4bHKsX7vkQWNZRd1/MiY+OuF02fx8K08My0RpkNnwj/rQ/gVSU1oEdFwkww==', + 'signature' => 'J9AcdJt5youJmMnBhS+Cc9ytArynIKtCRoNf/m0oOFO/e0hWHqs1NRdQBe81qzYIjf0+bj0Q97X9Xv1rnVJesPkQUbGaa4nAPt+viGSfvzTptjX4LKgqm8B3UkduBA262IcaWgM5P84gUqelkQIC1nIqq/MJTuC6oQ5lUwIV1a92ZurDjhwH4b3f7/ZLTTOTRD0DWN9W/yOyF1qECivgePR3eu+mkcBzXVU/TDZDJic9G7xhqcTnWV6qk+aKyzdNo1tu5W7mF+v5vF6rrGZrq55vPLWAHApTD7P+NFV01BnaCuN7/qGJNVs7m7EH03jpOw7y3jqNMmcmonYrJSMVqg==', + ], + Http::STATUS_OK, + ], + ]; + } + + /** + * @dataProvider dataRegisterDevice + * + * @param string $pushTokenHash + * @param string $devicePublicKey + * @param string $proxyServer + * @param bool $userIsValid + * @param int $tokenId + * @param bool $tokenIsValid + * @param bool $deviceCreated + * @param array $payload + * @param int $status + */ + public function testRegisterDevice($pushTokenHash, $devicePublicKey, $proxyServer, $userIsValid, $tokenId, $tokenIsValid, $deviceCreated, $payload, $status) { + $controller = $this->getController([ + 'savePushToken', + ]); + + $user = $this->createMock(IUser::class); + if ($userIsValid) { + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + } else { + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn(null); + } + + $this->session->expects($tokenId > 0 ? $this->once() : $this->never()) + ->method('get') + ->with('token-id') + ->willReturn($tokenId); + + if ($tokenIsValid) { + $token = $this->createMock(IToken::class); + $token->expects($this->once()) + ->method('getId') + ->willReturn($tokenId); + $this->tokenProvider->expects($this->any()) + ->method('getTokenById') + ->with($tokenId) + ->willReturn($token); + + $key = $this->createMock(Key::class); + $key->expects($this->once()) + ->method('getPrivate') + ->willReturn($this->userPrivateKey); + $key->expects($this->once()) + ->method('getPublic') + ->willReturn($this->userPublicKey); + + $this->identityProof->expects($this->once()) + ->method('getKey') + ->with($user) + ->willReturn($key); + + $controller->expects($this->once()) + ->method('savePushToken') + ->with($user, $token, $this->anything(), $devicePublicKey, $pushTokenHash, $proxyServer) + ->willReturn($deviceCreated); + } else { + $controller->expects($this->never()) + ->method('savePushToken'); + + $this->tokenProvider->expects($this->any()) + ->method('getTokenById') + ->with($tokenId) + ->willThrowException(new InvalidTokenException()); + } + + $response = $controller->registerDevice($pushTokenHash, $devicePublicKey, $proxyServer); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertSame($status, $response->getStatus()); + $this->assertSame($payload, $response->getData()); + } + + public function dataRemoveDevice() { + return [ + 'not authenticated' => [ + false, + 0, + false, + null, + [], + Http::STATUS_UNAUTHORIZED + ], + 'invalid token' => [ + true, + 23, + false, + null, + ['message' => 'INVALID_SESSION_TOKEN'], + Http::STATUS_BAD_REQUEST, + ], + 'using production' => [ + true, + 23, + false, + null, + ['message' => 'INVALID_SESSION_TOKEN'], + Http::STATUS_BAD_REQUEST, + ], + 'created or updated' => [ + true, + 23, + true, + true, + [], + Http::STATUS_ACCEPTED, + ], + 'not updated' => [ + true, + 42, + true, + false, + [], + Http::STATUS_OK, + ], + ]; + } + + + /** + * @dataProvider dataRemoveDevice + * + * @param bool $userIsValid + * @param int $tokenId + * @param bool $tokenIsValid + * @param bool $deviceDeleted + * @param array $payload + * @param int $status + */ + public function testRemoveDevice($userIsValid, $tokenId, $tokenIsValid, $deviceDeleted, $payload, $status) { + $controller = $this->getController([ + 'deletePushToken', + ]); + + $user = $this->createMock(IUser::class); + if ($userIsValid) { + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn($user); + } else { + $this->userSession->expects($this->any()) + ->method('getUser') + ->willReturn(null); + } + + $this->session->expects($tokenId > 0 ? $this->once() : $this->never()) + ->method('get') + ->with('token-id') + ->willReturn($tokenId); + + if ($tokenIsValid) { + $token = $this->createMock(IToken::class); + $this->tokenProvider->expects($this->any()) + ->method('getTokenById') + ->with($tokenId) + ->willReturn($token); + + $controller->expects($this->once()) + ->method('deletePushToken') + ->with($user, $token) + ->willReturn($deviceDeleted); + } else { + $controller->expects($this->never()) + ->method('deletePushToken'); + + $this->tokenProvider->expects($this->any()) + ->method('getTokenById') + ->with($tokenId) + ->willThrowException(new InvalidTokenException()); + } + + $response = $controller->removeDevice(); + $this->assertInstanceOf(DataResponse::class, $response); + $this->assertSame($status, $response->getStatus()); + $this->assertSame($payload, $response->getData()); + } + +}