فهرست منبع

Handle rejected pushes gracefully (#894)

If a push was rejected with a HTTP 400 response (which can indicate an
expired push token), don't show a protocol error. Instead, offer to
forget & recreate the session.
Danilo Bargen 6 سال پیش
والد
کامیت
eeeb5655d7
7فایلهای تغییر یافته به همراه94 افزوده شده و 7 حذف شده
  1. 3 1
      public/i18n/de.json
  2. 3 1
      public/i18n/en.json
  3. 10 0
      src/exceptions.ts
  4. 25 0
      src/partials/dialog.push-rejected.html
  5. 34 0
      src/partials/messenger.ts
  6. 3 2
      src/services/push.ts
  7. 16 3
      src/services/webclient.ts

+ 3 - 1
public/i18n/de.json

@@ -354,6 +354,8 @@
         "WEBCLIENT_DISABLED": "Threema Web wurde auf Ihrem Gerät deaktiviert.",
         "SESSION_REPLACED": "Die Sitzung wurde beendet, weil Sie eine andere Sitzung gestartet haben.",
         "OUT_OF_MEMORY": "Die Sitzung musste beendet werden, da auf dem Gerät nicht genügend Arbeitsspeicher zur Verfügung steht.",
-        "SESSION_ERROR": "Die Sitzung wurde aufgrund eines Protokollfehlers beendet."
+        "SESSION_ERROR": "Die Sitzung wurde aufgrund eines Protokollfehlers beendet.",
+        "PUSH_ERROR_INFO": "Threema Web konnte Ihr Gerät nicht mit einer Push-Benachrichtigung aufwecken. Dies passiert oft beim Wechsel auf ein neues Gerät.",
+        "PUSH_ERROR_ACTION": "Falls der Fehler weiterhin besteht, klicken Sie bitte auf \"Sitzung vergessen\" und erstellen Sie eine neue Sitzung."
     }
 }

+ 3 - 1
public/i18n/en.json

@@ -355,6 +355,8 @@
         "WEBCLIENT_DISABLED": "Threema Web was disabled on your device.",
         "SESSION_REPLACED": "This session was stopped because you started a Threema Web session in another browser window.",
         "OUT_OF_MEMORY": "The session had to be stopped because your device ran out of memory.",
-        "SESSION_ERROR": "The session was stopped due to a protocol error."
+        "SESSION_ERROR": "The session was stopped due to a protocol error.",
+        "PUSH_ERROR_INFO": "Attempts to wake up your device using push notifications have failed. This can happen, for example, if you have switched to a new mobile device recently.",
+        "PUSH_ERROR_ACTION": "Should this error persist, please click \"Forget Session\" and initiate a new Threema Web session."
     }
 }

+ 10 - 0
src/exceptions.ts

@@ -16,3 +16,13 @@
  */
 
 export class TimeoutError extends Error {}
+
+export class PushError extends Error {
+    // HTTP response status code returned by push relay server.
+    public readonly statusCode: number;
+
+    public constructor(msg: string, statusCode: number) {
+        super(msg);
+        this.statusCode = statusCode;
+    }
+}

+ 25 - 0
src/partials/dialog.push-rejected.html

@@ -0,0 +1,25 @@
+<md-dialog translate-attr="{'aria-label': 'common.ERROR'}">
+    <form ng-cloak>
+        <md-toolbar>
+            <div class="md-toolbar-tools">
+                <h2>common.ERROR</h2>
+            </div>
+        </md-toolbar>
+        <md-dialog-content>
+            <div class="md-dialog-content">
+                <p translate>connection.PUSH_ERROR_INFO</p>
+                <p translate>connection.PUSH_ERROR_ACTION</p>
+            </div>
+        </md-dialog-content>
+        <md-dialog-actions layout="row">
+            <span flex></span>
+            <md-button role="button" class="md-primary" ng-click="ctrl.forget()" aria-labelledby="aria-label-forget">
+                <span translate id="aria-label-forget">welcome.FORGET_SESSION_BTN</span>
+            </md-button>
+            <md-button role="button" class="md-primary" ng-click="ctrl.cancel()" aria-labelledby="aria-label-cancel">
+                <span translate id="aria-label-cancel">common.CANCEL</span>
+            </md-button>
+        </md-dialog-actions>
+    </form>
+</md-dialog>
+

+ 34 - 0
src/partials/messenger.ts

@@ -165,6 +165,40 @@ export class DeviceUnreachableController extends DialogController {
     }
 }
 
+/**
+ * Handle device unreachable
+ */
+export class PushRejectedDialogController extends DialogController {
+    private readonly log: Logger;
+
+    private readonly $window: ng.IWindowService;
+
+    private readonly trustedKeyStore: TrustedKeyStoreService;
+
+    public static readonly $inject = ['$mdDialog', '$window', 'TrustedKeyStore' , 'LogService'];
+    constructor(
+        $mdDialog: ng.material.IDialogService,
+        $window: ng.IWindowService,
+        trustedKeyStore: TrustedKeyStoreService,
+        logService: LogService,
+    ) {
+        super($mdDialog);
+        this.$window = $window;
+        this.log = logService.getLogger('PushRejectedDialog-C');
+        this.trustedKeyStore = trustedKeyStore;
+    }
+
+    /**
+     * Remove the stored session.
+     */
+    public forget(): void {
+        this.log.info('Forgetting stored session');
+        this.trustedKeyStore.clearTrustedKey();
+        this.cancel();
+        this.$window.location.reload();
+    }
+}
+
 /**
  * Handle settings
  */

+ 3 - 2
src/services/push.ts

@@ -17,7 +17,7 @@
 
 import {Logger} from 'ts-log';
 
-import {TimeoutError} from '../exceptions';
+import {PushError, TimeoutError} from '../exceptions';
 import {randomString, sleep} from '../helpers';
 import {sha256} from '../helpers/crypto';
 import {LogService} from './log';
@@ -124,6 +124,7 @@ export class PushSession {
      *
      * @throws TimeoutError in case the maximum amount of retries has been
      *   reached.
+     * @throws PushError if the push was rejected by the push relay server.
      * @throws Error in case of an unrecoverable error which prevents further
      *   pushes.
      */
@@ -215,7 +216,7 @@ export class PushSession {
                     // Client error: Don't retry
                     const error = `Push rejected (client error), status: ${response.status}`;
                     this.log.warn(error);
-                    this.doneFuture.reject(new Error(error));
+                    this.doneFuture.reject(new PushError(error, response.status));
                 } else {
                     // Server error: Retry
                     this.log.warn(`Push rejected (server error), status: ${response.status}`);

+ 16 - 3
src/services/webclient.ts

@@ -58,10 +58,10 @@ import {TimeoutService} from './timeout';
 import {TitleService} from './title';
 import {VersionService} from './version';
 
-import {TimeoutError} from '../exceptions';
+import {PushError, TimeoutError} from '../exceptions';
 import {ConfidentialWireMessage} from '../helpers/confidential';
 import {UnboundedFlowControlledDataChannel} from '../helpers/data_channel';
-import {DeviceUnreachableController} from '../partials/messenger';
+import {DeviceUnreachableController, PushRejectedDialogController} from '../partials/messenger';
 import {ChunkCache} from '../protocol/cache';
 import {SequenceNumber} from '../protocol/sequence_number';
 
@@ -1172,9 +1172,12 @@ export class WebClientService {
                     // Reset push session
                     this.resetPushSession(false);
 
-                    // Handle error
+                    // Handle errors
                     if (error instanceof TimeoutError) {
                         this.showDeviceUnreachableDialog();
+                    } else if (error instanceof PushError && error.statusCode === 400) {
+                        // Can happen if the push token is invalid
+                        this.showPushRejectedDialog();
                     } else {
                         this.failSession();
                     }
@@ -1229,6 +1232,16 @@ export class WebClientService {
         }
     }
 
+    public showPushRejectedDialog(): void {
+        this.$mdDialog.show({
+            controller: PushRejectedDialogController,
+            controllerAs: 'ctrl',
+            templateUrl: 'partials/dialog.push-rejected.html',
+            parent: angular.element(document.body),
+            escapeToClose: false,
+        });
+    }
+
     /**
      * Start the webclient service.
      * Return a promise that resolves once connected.