2020-01-09 23:27:07 +01:00
|
|
|
import * as signalR from '@microsoft/signalr';
|
|
|
|
import * as signalRMsgPack from '@microsoft/signalr-protocol-msgpack';
|
2018-08-20 19:45:32 +02:00
|
|
|
|
|
|
|
import { NotificationType } from '../enums/notificationType';
|
|
|
|
|
2018-08-21 04:20:04 +02:00
|
|
|
import { ApiService } from '../abstractions/api.service';
|
2018-08-20 22:01:26 +02:00
|
|
|
import { AppIdService } from '../abstractions/appId.service';
|
2018-08-20 19:45:32 +02:00
|
|
|
import { EnvironmentService } from '../abstractions/environment.service';
|
2020-12-04 19:38:26 +01:00
|
|
|
import { LogService } from '../abstractions/log.service'
|
2018-08-20 19:45:32 +02:00
|
|
|
import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service';
|
|
|
|
import { SyncService } from '../abstractions/sync.service';
|
|
|
|
import { UserService } from '../abstractions/user.service';
|
2020-03-27 15:03:27 +01:00
|
|
|
import { VaultTimeoutService } from '../abstractions/vaultTimeout.service';
|
2020-12-04 19:38:26 +01:00
|
|
|
import { ConsoleLogService } from '../services/consoleLog.service';
|
2018-08-20 19:45:32 +02:00
|
|
|
|
2018-08-20 22:01:26 +02:00
|
|
|
import {
|
|
|
|
NotificationResponse,
|
|
|
|
SyncCipherNotification,
|
|
|
|
SyncFolderNotification,
|
|
|
|
} from '../models/response/notificationResponse';
|
2018-08-20 19:45:32 +02:00
|
|
|
|
|
|
|
export class NotificationsService implements NotificationsServiceAbstraction {
|
|
|
|
private signalrConnection: signalR.HubConnection;
|
2018-08-21 04:20:04 +02:00
|
|
|
private url: string;
|
2018-08-22 19:46:35 +02:00
|
|
|
private connected = false;
|
|
|
|
private inited = false;
|
2018-08-23 03:09:58 +02:00
|
|
|
private inactive = false;
|
2018-08-22 19:46:35 +02:00
|
|
|
private reconnectTimer: any = null;
|
2018-08-20 19:45:32 +02:00
|
|
|
|
2018-08-23 03:46:34 +02:00
|
|
|
constructor(private userService: UserService, private syncService: SyncService,
|
|
|
|
private appIdService: AppIdService, private apiService: ApiService,
|
2020-12-04 19:38:26 +01:00
|
|
|
private vaultTimeoutService: VaultTimeoutService,
|
|
|
|
private logoutCallback: () => Promise<void>, private logService?: LogService) {
|
|
|
|
if (!logService) {
|
|
|
|
this.logService = new ConsoleLogService(false);
|
|
|
|
}
|
|
|
|
}
|
2018-08-20 19:45:32 +02:00
|
|
|
|
|
|
|
async init(environmentService: EnvironmentService): Promise<void> {
|
2018-08-22 19:46:35 +02:00
|
|
|
this.inited = false;
|
2018-08-21 04:20:04 +02:00
|
|
|
this.url = 'https://notifications.bitwarden.com';
|
2018-08-20 19:45:32 +02:00
|
|
|
if (environmentService.notificationsUrl != null) {
|
2018-08-21 04:20:04 +02:00
|
|
|
this.url = environmentService.notificationsUrl;
|
2018-08-20 19:45:32 +02:00
|
|
|
} else if (environmentService.baseUrl != null) {
|
2018-08-21 04:20:04 +02:00
|
|
|
this.url = environmentService.baseUrl + '/notifications';
|
2018-08-20 19:45:32 +02:00
|
|
|
}
|
2018-08-22 19:46:35 +02:00
|
|
|
|
2018-08-23 15:25:39 +02:00
|
|
|
// Set notifications server URL to `https://-` to effectively disable communication
|
|
|
|
// with the notifications server from the client app
|
|
|
|
if (this.url === 'https://-') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-22 19:46:35 +02:00
|
|
|
if (this.signalrConnection != null) {
|
|
|
|
this.signalrConnection.off('ReceiveMessage');
|
2019-07-12 05:05:38 +02:00
|
|
|
this.signalrConnection.off('Heartbeat');
|
2018-08-22 19:46:35 +02:00
|
|
|
await this.signalrConnection.stop();
|
|
|
|
this.connected = false;
|
|
|
|
this.signalrConnection = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.signalrConnection = new signalR.HubConnectionBuilder()
|
|
|
|
.withUrl(this.url + '/hub', {
|
2018-08-23 03:46:34 +02:00
|
|
|
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
|
2020-03-21 05:19:40 +01:00
|
|
|
skipNegotiation: true,
|
|
|
|
transport: signalR.HttpTransportType.WebSockets,
|
2018-08-22 19:46:35 +02:00
|
|
|
})
|
2018-08-24 03:43:40 +02:00
|
|
|
.withHubProtocol(new signalRMsgPack.MessagePackHubProtocol())
|
|
|
|
// .configureLogging(signalR.LogLevel.Trace)
|
2018-08-22 19:46:35 +02:00
|
|
|
.build();
|
|
|
|
|
2018-08-22 19:48:51 +02:00
|
|
|
this.signalrConnection.on('ReceiveMessage',
|
|
|
|
(data: any) => this.processNotification(new NotificationResponse(data)));
|
2019-07-12 05:05:38 +02:00
|
|
|
this.signalrConnection.on('Heartbeat',
|
|
|
|
(data: any) => { /*console.log('Heartbeat!');*/ });
|
2018-08-22 19:46:35 +02:00
|
|
|
this.signalrConnection.onclose(() => {
|
|
|
|
this.connected = false;
|
2018-08-23 14:56:23 +02:00
|
|
|
this.reconnect(true);
|
2018-08-22 19:46:35 +02:00
|
|
|
});
|
|
|
|
this.inited = true;
|
2018-08-23 03:46:34 +02:00
|
|
|
if (await this.isAuthedAndUnlocked()) {
|
2018-08-23 14:56:23 +02:00
|
|
|
await this.reconnect(false);
|
2018-08-22 19:46:35 +02:00
|
|
|
}
|
2018-08-20 19:45:32 +02:00
|
|
|
}
|
|
|
|
|
2018-08-23 14:56:23 +02:00
|
|
|
async updateConnection(sync = false): Promise<void> {
|
2018-08-23 15:25:39 +02:00
|
|
|
if (!this.inited) {
|
|
|
|
return;
|
|
|
|
}
|
2018-08-20 19:45:32 +02:00
|
|
|
try {
|
2018-08-23 03:46:34 +02:00
|
|
|
if (await this.isAuthedAndUnlocked()) {
|
2018-08-23 14:56:23 +02:00
|
|
|
await this.reconnect(sync);
|
2018-08-20 19:45:32 +02:00
|
|
|
} else {
|
|
|
|
await this.signalrConnection.stop();
|
|
|
|
}
|
|
|
|
} catch (e) {
|
2020-12-04 19:38:26 +01:00
|
|
|
this.logService.error(e.toString())
|
2018-08-20 19:45:32 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:46:34 +02:00
|
|
|
async reconnectFromActivity(): Promise<void> {
|
2018-08-23 03:09:58 +02:00
|
|
|
this.inactive = false;
|
2018-08-24 21:21:28 +02:00
|
|
|
if (this.inited && !this.connected) {
|
2018-08-23 14:56:23 +02:00
|
|
|
await this.reconnect(true);
|
2018-08-23 03:09:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-23 03:46:34 +02:00
|
|
|
async disconnectFromInactivity(): Promise<void> {
|
2018-08-23 03:09:58 +02:00
|
|
|
this.inactive = true;
|
2018-08-24 21:21:28 +02:00
|
|
|
if (this.inited && this.connected) {
|
2018-08-23 03:09:58 +02:00
|
|
|
await this.signalrConnection.stop();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-20 19:45:32 +02:00
|
|
|
private async processNotification(notification: NotificationResponse) {
|
2018-08-20 22:01:26 +02:00
|
|
|
const appId = await this.appIdService.getAppId();
|
|
|
|
if (notification == null || notification.contextId === appId) {
|
2018-08-20 19:45:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-28 14:47:06 +02:00
|
|
|
const isAuthenticated = await this.userService.isAuthenticated();
|
|
|
|
const payloadUserId = notification.payload.userId || notification.payload.UserId;
|
|
|
|
const myUserId = await this.userService.getUserId();
|
|
|
|
if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-08-20 19:45:32 +02:00
|
|
|
switch (notification.type) {
|
|
|
|
case NotificationType.SyncCipherCreate:
|
|
|
|
case NotificationType.SyncCipherUpdate:
|
2018-08-21 14:20:43 +02:00
|
|
|
await this.syncService.syncUpsertCipher(notification.payload as SyncCipherNotification,
|
|
|
|
notification.type === NotificationType.SyncCipherUpdate);
|
2018-08-20 22:01:26 +02:00
|
|
|
break;
|
|
|
|
case NotificationType.SyncCipherDelete:
|
2018-08-20 19:45:32 +02:00
|
|
|
case NotificationType.SyncLoginDelete:
|
2018-08-21 04:20:04 +02:00
|
|
|
await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
|
2018-08-20 19:45:32 +02:00
|
|
|
break;
|
|
|
|
case NotificationType.SyncFolderCreate:
|
|
|
|
case NotificationType.SyncFolderUpdate:
|
2018-08-21 14:20:43 +02:00
|
|
|
await this.syncService.syncUpsertFolder(notification.payload as SyncFolderNotification,
|
|
|
|
notification.type === NotificationType.SyncFolderUpdate);
|
2018-08-20 22:01:26 +02:00
|
|
|
break;
|
|
|
|
case NotificationType.SyncFolderDelete:
|
2018-08-21 04:20:04 +02:00
|
|
|
await this.syncService.syncDeleteFolder(notification.payload as SyncFolderNotification);
|
2018-08-20 19:45:32 +02:00
|
|
|
break;
|
|
|
|
case NotificationType.SyncVault:
|
|
|
|
case NotificationType.SyncCiphers:
|
|
|
|
case NotificationType.SyncSettings:
|
2018-08-28 14:47:06 +02:00
|
|
|
if (isAuthenticated) {
|
|
|
|
await this.syncService.fullSync(false);
|
|
|
|
}
|
2018-08-21 04:20:04 +02:00
|
|
|
break;
|
|
|
|
case NotificationType.SyncOrgKeys:
|
2018-08-28 14:47:06 +02:00
|
|
|
if (isAuthenticated) {
|
|
|
|
await this.apiService.refreshIdentityToken();
|
|
|
|
await this.syncService.fullSync(true);
|
|
|
|
// Stop so a reconnect can be made
|
|
|
|
await this.signalrConnection.stop();
|
|
|
|
}
|
2018-08-20 19:45:32 +02:00
|
|
|
break;
|
2018-08-28 14:38:19 +02:00
|
|
|
case NotificationType.LogOut:
|
2018-08-28 14:47:06 +02:00
|
|
|
if (isAuthenticated) {
|
|
|
|
this.logoutCallback();
|
|
|
|
}
|
2018-08-28 14:38:19 +02:00
|
|
|
break;
|
2018-08-20 19:45:32 +02:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2018-08-21 04:20:04 +02:00
|
|
|
|
2018-08-23 14:56:23 +02:00
|
|
|
private async reconnect(sync: boolean) {
|
2018-08-22 19:46:35 +02:00
|
|
|
if (this.reconnectTimer != null) {
|
|
|
|
clearTimeout(this.reconnectTimer);
|
|
|
|
this.reconnectTimer = null;
|
|
|
|
}
|
2018-08-23 03:09:58 +02:00
|
|
|
if (this.connected || !this.inited || this.inactive) {
|
|
|
|
return;
|
|
|
|
}
|
2018-08-23 03:46:34 +02:00
|
|
|
const authedAndUnlocked = await this.isAuthedAndUnlocked();
|
|
|
|
if (!authedAndUnlocked) {
|
2018-08-22 19:46:35 +02:00
|
|
|
return;
|
2018-08-21 04:20:04 +02:00
|
|
|
}
|
|
|
|
|
2018-08-22 19:46:35 +02:00
|
|
|
try {
|
2018-08-23 14:56:23 +02:00
|
|
|
await this.signalrConnection.start();
|
|
|
|
this.connected = true;
|
|
|
|
if (sync) {
|
|
|
|
await this.syncService.fullSync(false);
|
|
|
|
}
|
2018-08-22 19:46:35 +02:00
|
|
|
} catch { }
|
2018-08-21 04:20:04 +02:00
|
|
|
|
2018-08-22 19:46:35 +02:00
|
|
|
if (!this.connected) {
|
2018-09-01 05:24:43 +02:00
|
|
|
this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000));
|
2018-08-22 19:46:35 +02:00
|
|
|
}
|
2018-08-21 04:20:04 +02:00
|
|
|
}
|
2018-08-23 03:46:34 +02:00
|
|
|
|
|
|
|
private async isAuthedAndUnlocked() {
|
|
|
|
if (await this.userService.isAuthenticated()) {
|
2020-03-27 15:03:27 +01:00
|
|
|
const locked = await this.vaultTimeoutService.isLocked();
|
2019-02-18 03:16:49 +01:00
|
|
|
return !locked;
|
2018-08-23 03:46:34 +02:00
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2018-09-01 05:24:43 +02:00
|
|
|
|
|
|
|
private random(min: number, max: number) {
|
|
|
|
min = Math.ceil(min);
|
|
|
|
max = Math.floor(max);
|
|
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
|
|
}
|
2018-08-20 19:45:32 +02:00
|
|
|
}
|