Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3164e40
Allow devices to register for push notifications
nickvergessen Jan 25, 2017
2fd4a9f
Store the hash to be able to compare it easily
nickvergessen Jan 26, 2017
ebaa6d1
Get the token id from the session
nickvergessen Jan 26, 2017
08ea5ff
Error when on deletion the device public key is wrong as well
nickvergessen Jan 26, 2017
b4cc0aa
encode the signature before sending
nickvergessen Jan 26, 2017
1fd8c35
Fix return codes and messages
nickvergessen Jan 26, 2017
ed7fc81
Add documentation for devices
nickvergessen Jan 26, 2017
0557efa
Add the signature to the remove request as well
nickvergessen Feb 1, 2017
4a0943d
PHP already hashes the message itself
nickvergessen Feb 10, 2017
6e1e4ef
Start the actual pushing
nickvergessen Mar 1, 2017
af272b7
Sign and encrypt the subject
nickvergessen Mar 2, 2017
a6b1645
Encode signatures and encrypted messages and fix query and encryption
nickvergessen Mar 3, 2017
362ed9b
Add docs
nickvergessen Mar 3, 2017
8d2ced5
Add some more help on debugging and fix server URL
nickvergessen Mar 6, 2017
682d276
Also store the device identifier
nickvergessen Mar 9, 2017
3e53f38
Fix signing and make the sending okay for golang
nickvergessen Mar 9, 2017
6260540
Receive the proxy server from the app
nickvergessen Mar 29, 2017
468017b
Fix wrong function name
nickvergessen Mar 29, 2017
bd64d6a
Allow localhost via http
nickvergessen Apr 5, 2017
9a63444
NC12 can do this
nickvergessen Apr 5, 2017
185751a
Clarify docs
nickvergessen Apr 7, 2017
7f0a9f0
Fix trailing new line
nickvergessen Apr 18, 2017
475ad40
Add capability for push
nickvergessen Apr 18, 2017
8ae1023
Fix routes
nickvergessen Apr 18, 2017
ad83ef6
Wrong demorgan
nickvergessen Apr 19, 2017
280e707
Use DataResponse so the result is valid OCS
nickvergessen Apr 19, 2017
a2346e6
Fix existing tests
nickvergessen Apr 19, 2017
3a75592
Add more tests
nickvergessen Apr 19, 2017
37f5a93
Delete invalid push tokens to avoid getting banned
nickvergessen Apr 19, 2017
c4625ff
Allow sending multiple notifications via the same proxy
nickvergessen Apr 19, 2017
6a4c8af
Add unit tests for Push
nickvergessen Apr 19, 2017
edbb991
Add unit tests for the PushController
nickvergessen Apr 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions appinfo/database.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<create>true</create>
<overwrite>false</overwrite>
<charset>utf8</charset>

<table>
<name>*dbprefix*notifications</name>
<declaration>
Expand Down Expand Up @@ -118,4 +119,64 @@
</index>
</declaration>
</table>

<table>
<name>*dbprefix*notifications_pushtokens</name>
<declaration>
<field>
<name>uid</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
</field>
<field>
<name>token</name>
<type>integer</type>
<default>0</default>
<notnull>true</notnull>
<length>4</length>
</field>
<field>
<name>deviceidentifier</name>
<type>text</type>
<notnull>true</notnull>
<length>128</length>
</field>
<field>
<name>devicepublickey</name>
<type>text</type>
<notnull>true</notnull>
<length>512</length>
</field>
<field>
<name>devicepublickeyhash</name>
<type>text</type>
<notnull>true</notnull>
<length>128</length>
</field>
<field>
<name>pushtokenhash</name>
<type>text</type>
<notnull>true</notnull>
<length>128</length>
</field>
<field>
<name>proxyserver</name>
<type>text</type>
<notnull>true</notnull>
<length>256</length>
</field>

<index>
<name>oc_notifpushtoken</name>
<unique>true</unique>
<field>
<name>uid</name>
</field>
<field>
<name>token</name>
</field>
</index>
</declaration>
</table>
</database>
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

<licence>AGPL</licence>
<author>Joas Schilling</author>
<version>1.2.0</version>
<version>2.0.0</version>

<types>
<logging/>
Expand Down
2 changes: 2 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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/{apiVersion}/push', 'verb' => 'POST', 'requirements' => ['apiVersion' => 'v2']],
['name' => 'Push#removeDevice', 'url' => '/api/{apiVersion}/push', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => 'v2']],
],
];
213 changes: 213 additions & 0 deletions docs/push-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# 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`

```
{
"ocs": {
...
"data": {
...
"capabilities": {
...
"notifications": {
"push": [
...
"devices"
]
}
}
}
}
}
```



## 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`, `PushTokenHash` and `proxyServerUrl` to the Nextcloud server:

```
POST /ocs/v2.php/apps/notifications/api/v2/push

{
"pushTokenHash": "{{PushTokenHash}}",
"devicePublicKey": "{{devicePublicKey}}",
"proxyServer": "{{proxyServerUrl}}"
}
```


### 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 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 |



#### 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. |
| `INVALID_PROXY_SERVER` | The proxy server was not a valid https URL. |



## 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 a `DELETE` request to the Nextcloud server:

```
DELETE /ocs/v2.php/apps/notifications/api/v2/push
```



### Response

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 | 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. |



## 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:

```
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`, `deviceIdentifierSignature` and the user´s `publicKey` (from the server´s response) to the Push Proxy:

```
DELETE /devices

{
"deviceIdentifier": "{{deviceIdentifier}}",
"deviceIdentifierSignature": "{{signature}}",
"userPublicKey": "{{userPublicKey}}"
}
```



### Response

The server replies with the following status codes:

| Status code | Meaning |
| ----------- | ---------------------------------------- |
| 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 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:

```json
{
"to" : "APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx...",
"notification" : {
"body" : "NEW_NOTIFICATION",
"body_loc_key" : "NEW_NOTIFICATION",
"title" : "NEW_NOTIFICATION",
"title_loc_key" : "NEW_NOTIFICATION"
},
"data" : {
"subject" : "*Encrypted subject*",
"signature" : "*Signature*"
}
}
```

| Attribute | Meaning |
| ----------- | ---------------------------------------- |
| `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.
10 changes: 8 additions & 2 deletions lib/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,24 @@

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 {
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
$container->registerService(IProvider::class, function(IAppContainer $c) {
return $c->getServer()->query(IProvider::class);
});
}

public function register() {
Expand Down
3 changes: 3 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public function getCapabilities() {
'icons',
'rich-strings',
],
'push' => [
'devices',
],
],
];
}
Expand Down
Loading