diff --git a/plugins/synology-ss/package-lock.json b/plugins/synology-ss/package-lock.json index 2dbf572bf..652f2d69d 100644 --- a/plugins/synology-ss/package-lock.json +++ b/plugins/synology-ss/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/synology-ss", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/synology-ss", - "version": "0.0.4", + "version": "0.0.5", "license": "Apache", "dependencies": { "axios": "^0.24.0" @@ -17,7 +17,8 @@ } }, "../../sdk": { - "version": "0.0.121", + "name": "@scrypted/sdk", + "version": "0.0.133", "dev": true, "license": "ISC", "dependencies": { diff --git a/plugins/synology-ss/package.json b/plugins/synology-ss/package.json index 2d2de636a..b2ce93873 100644 --- a/plugins/synology-ss/package.json +++ b/plugins/synology-ss/package.json @@ -1,6 +1,6 @@ { "name": "@scrypted/synology-ss", - "version": "0.0.4", + "version": "0.0.5", "description": "A Synology Surveillance Station plugin for Scrypted", "author": "Scrypted", "license": "Apache", diff --git a/plugins/synology-ss/src/api/synology-api-client.ts b/plugins/synology-ss/src/api/synology-api-client.ts index 1de7df8a6..ef56d658a 100644 --- a/plugins/synology-ss/src/api/synology-api-client.ts +++ b/plugins/synology-ss/src/api/synology-api-client.ts @@ -36,7 +36,15 @@ export class SynologyApiClient { idList: cameraIds.join(',') }; - return await this.sendRequest(params); + const errorCodeDescs = { + '400': 'Execution failed', + // Usually when 401 happens, there's a "Fail to get local host Ip str!" error in surveillance.log. + // One instance it was due to an old network bridge configured in Docker that had to be removed. + '401': 'Parameter invalid (possibly due to misconfigured Synology network interface -- run ifconfig on your server)', + '402': 'Camera disabled' + }; + + return await this.sendRequest(params, null, false, errorCodeDescs); } public async getCameraSnapshot(cameraId: number | string) { @@ -67,7 +75,8 @@ export class SynologyApiClient { return response.cameras; } - public async login(account: string, password: string): Promise { + public async login(account: string, password: string, otpCode?: number, enableDeviceToken: boolean = false, deviceName?: string, + deviceId?: string): Promise { const params = { api: 'SYNO.API.Auth', version: 6, @@ -77,6 +86,22 @@ export class SynologyApiClient { passwd: password }; + if (otpCode) { + params['otp_code'] = otpCode; + } + + if (enableDeviceToken) { + params['enable_device_token'] = enableDeviceToken ? 'yes' : 'no'; + } + + if (deviceName) { + params['device_name'] = deviceName; + } + + if (deviceId) { + params['device_id'] = deviceId; + } + const errorCodeDescs = { '400': 'Invalid password', '401': 'Guest or disabled account', @@ -92,7 +117,9 @@ export class SynologyApiClient { '411': 'Account Locked (when account max try exceed)' }; - await this.sendRequest(params, null, true, errorCodeDescs); + const response = await this.sendRequest(params, null, true, errorCodeDescs); + + return response.did; } private async queryApiInfo(): Promise> { @@ -153,6 +180,11 @@ interface SynologyApiResponse { success: boolean; } +interface SynologyApiAuthResponse { + sid?: string; + did?: string; +} + interface SynologyApiListCamerasResponse { total: number; cameras: SynologyCamera[]; diff --git a/plugins/synology-ss/src/main.ts b/plugins/synology-ss/src/main.ts index d1f97f1f1..19e654dcf 100644 --- a/plugins/synology-ss/src/main.ts +++ b/plugins/synology-ss/src/main.ts @@ -211,6 +211,8 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings const url = this.getSetting('url'); const username = this.getSetting('username'); const password = this.getSetting('password'); + const otpCode = this.getSetting('otpCode'); + const mfaDeviceId = this.getSetting('mfaDeviceId'); this.log.clearAlerts(); @@ -234,8 +236,31 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings } try { - await this.api.login(username, password); + const newMfaDeviceId = await this.api.login(username, password, otpCode ? parseInt(otpCode) : undefined, !!otpCode, 'Scrypted', mfaDeviceId); + // If a OTP was present, store the device ID to allow us to skip the OTP requirement next login. + if (otpCode) { + this.storage.setItem('mfaDeviceId', newMfaDeviceId); + } + } + catch (e) { + this.log.a(`login error: ${e}`); + this.console.error('login error', e); + + // Clear device ID upon login failure, since it's likely useless now + this.storage.removeItem('mfaDeviceId'); + + return; + } + finally { + // Clear the OTP setting if provided since it's a temporary code + if (otpCode) { + this.storage.removeItem('otpCode'); + this.onDeviceEvent(ScryptedInterface.Settings, undefined); + } + } + + try { this.cameras = await this.api.listCameras(); if (!this.cameras) { @@ -289,8 +314,8 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings } } catch (e) { - this.log.a(`login error: ${e}`); - this.console.error('login error', e); + this.log.a(`device discovery error: ${e}`); + this.console.error('device discovery error', e); } } @@ -323,6 +348,13 @@ class SynologySurveillanceStation extends ScryptedDeviceBase implements Settings type: 'password', value: this.getSetting('password'), }, + { + key: 'otpCode', + title: 'Verification Code (OTP)', + description: 'Required only if you have two-factor authentication enabled', + type: 'integer', + value: this.getSetting('otpCode'), + }, { key: 'url', title: 'Synology Surveillance Station URL',