Skip to content

Commit

Permalink
Merge pull request #59 from nextcloud/push-notification
Browse files Browse the repository at this point in the history
Allow devices to register for push notifications
  • Loading branch information
nickvergessen authored Apr 24, 2017
2 parents 6982181 + edbb991 commit af90c15
Show file tree
Hide file tree
Showing 16 changed files with 1,805 additions and 28 deletions.
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

0 comments on commit af90c15

Please sign in to comment.