Skip to content

Commit

Permalink
reolink: use Login json api to get around URL escaping limitations wi…
Browse files Browse the repository at this point in the history
…th some firmware (#1509)
  • Loading branch information
gtalusan authored Jun 28, 2024
1 parent 5351d86 commit fc93a85
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 39 deletions.
23 changes: 17 additions & 6 deletions plugins/reolink/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,23 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
return ret;
}

addRtspCredentials(rtspUrl: string) {
const url = new URL(rtspUrl);
if (url.protocol !== 'rtmp:') {
url.username = this.storage.getItem('username');
url.password = this.storage.getItem('password') || '';
} else {
const params = url.searchParams;
params.set('token', this.client.token);
}
return url.toString();
}

async createVideoStream(vso: UrlMediaStreamOptions): Promise<MediaObject> {
await this.client.login();
return super.createVideoStream(vso);
}

async getConstructedVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
this.videoStreamOptions ||= this.getConstructedVideoStreamOptionsInternal().catch(e => {
this.constructedVideoStreamOptions = undefined;
Expand Down Expand Up @@ -521,8 +538,6 @@ class ReolinkCamera extends RtspSmartCamera implements Camera, DeviceProvider, R
const params = streamUrl.searchParams;
params.set("channel", this.getRtspChannel().toString())
params.set("stream", '0')
params.set("user", this.getUsername())
params.set("password", this.getPassword())
stream.url = streamUrl.toString();
stream.name = `RTMP ${stream.id}`;
} else if (stream.container === 'rtsp') {
Expand Down Expand Up @@ -643,10 +658,6 @@ class ReolinkProvider extends RtspProvider {
const skipValidate = settings.skipValidate?.toString() === 'true';
const username = settings.username?.toString();
const password = settings.password?.toString();
// verify password only has alphanumeric characters because reolink can't handle
// url escaping.
if (!skipValidate && !/^[a-zA-Z0-9]+$/.test(password))
throw new Error('Change the password this Reolink device to be alphanumeric characters only. See https://docs.scrypted.app/camera-preparation.html#authentication-setup for more information.');
let doorbell: boolean = false;
let name: string = 'Reolink Camera';
let deviceInfo: DevInfo;
Expand Down
109 changes: 76 additions & 33 deletions plugins/reolink/src/reolink-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import https, { RequestOptions } from 'https';
import { PassThrough, Readable } from 'stream';
import { HttpFetchOptions, HttpFetchResponseType } from '../../../server/src/fetch/http-fetch';

import { getMotionState, reolinkHttpsAgent } from './probe';
import { PanTiltZoomCommand } from "@scrypted/sdk";
import { sleep } from "@scrypted/common/src/sleep";

Expand Down Expand Up @@ -65,6 +64,8 @@ export type SirenResponse = {

export class ReolinkCameraClient {
credential: AuthFetchCredentialState;
token: string;
tokenLease: number = Date.now();

constructor(public host: string, public username: string, public password: string, public channelId: number, public console: Console) {
this.credential = {
Expand All @@ -73,26 +74,71 @@ export class ReolinkCameraClient {
};
}

async request(urlOrOptions: string | URL | HttpFetchOptions<Readable>, body?: Readable) {
private async request(options: HttpFetchOptions<Readable>, body?: Readable) {
const response = await authHttpFetch({
...typeof urlOrOptions !== 'string' && !(urlOrOptions instanceof URL) ? urlOrOptions : {
url: urlOrOptions,
},
...options,
rejectUnauthorized: false,
credential: this.credential,
body,
});
return response;
}

async login() {
if (this.tokenLease > Date.now()) {
return;
}

this.console.log(`token expired at ${this.tokenLease}, renewing...`);

const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'Login');

const createReadable = (data: any) => {
const pt = new PassThrough();
pt.write(Buffer.from(JSON.stringify(data)));
pt.end();
return pt;
}

const response = await this.request({
url,
method: 'POST',
responseType: 'json',
}, createReadable([
{
cmd: 'Login',
action: 0,
param: {
User: {
userName: this.username,
password: this.password
}
}
},
]));
this.token = response.body?.[0]?.value?.Token?.name || response.body?.value?.Token?.name;
if (!this.token) {
throw new Error('unable to login');
}
this.tokenLease = Date.now() + 1000 * (response.body?.[0]?.value?.Token.leaseTime || response.body?.value?.Token.leaseTime);
}

async requestWithLogin(options: HttpFetchOptions<Readable>, body?: Readable) {
await this.login();
const url = options.url as URL;
const params = url.searchParams;
params.set('token', this.token);
return this.request(options, body);
}

async reboot() {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'Reboot');
params.set('user', this.username);
params.set('password', this.password);
const response = await this.request({
url: url.toString(),
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
return {
Expand All @@ -111,17 +157,26 @@ export class ReolinkCameraClient {
// }
// ]
async getMotionState() {
return getMotionState(this.credential, this.username, this.password, this.host, this.channelId);
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetMdState');
params.set('channel', this.channelId.toString());
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
return {
value: !!response.body?.[0]?.value?.state,
data: response.body,
};
}

async getAiState() {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetAiState');
params.set('channel', this.channelId.toString());
params.set('user', this.username);
params.set('password', this.password);
const response = await this.request({
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
Expand All @@ -136,9 +191,7 @@ export class ReolinkCameraClient {
const params = url.searchParams;
params.set('cmd', 'GetAbility');
params.set('channel', this.channelId.toString());
params.set('user', this.username);
params.set('password', this.password);
const response = await this.request({
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
Expand All @@ -154,10 +207,8 @@ export class ReolinkCameraClient {
params.set('cmd', 'Snap');
params.set('channel', this.channelId.toString());
params.set('rs', Date.now().toString());
params.set('user', this.username);
params.set('password', this.password);

const response = await this.request({
const response = await this.requestWithLogin({
url,
timeout,
});
Expand All @@ -171,9 +222,7 @@ export class ReolinkCameraClient {
params.set('cmd', 'GetEnc');
// is channel used on this call?
params.set('channel', this.channelId.toString());
params.set('user', this.username);
params.set('password', this.password);
const response = await this.request({
const response = await this.requestWithLogin({
url,
responseType: 'json',
});
Expand All @@ -185,22 +234,18 @@ export class ReolinkCameraClient {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'GetDevInfo');
params.set('user', this.username);
params.set('password', this.password);
const response = await this.request({
const response = await this.requestWithLogin({
url,
responseType: 'json',
});

return response.body?.[0]?.value?.DevInfo;
}

async ptzOp(op: string) {
private async ptzOp(op: string) {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'PtzCtrl');
params.set('user', this.username);
params.set('password', this.password);

const createReadable = (data: any) => {
const pt = new PassThrough();
Expand All @@ -209,7 +254,7 @@ export class ReolinkCameraClient {
return pt;
}

const c1 = this.request({
const c1 = this.requestWithLogin({
url,
method: 'POST',
responseType: 'text',
Expand All @@ -227,7 +272,7 @@ export class ReolinkCameraClient {

await sleep(500);

const c2 = this.request({
const c2 = this.requestWithLogin({
url,
method: 'POST',
}, createReadable([
Expand Down Expand Up @@ -273,8 +318,6 @@ export class ReolinkCameraClient {
const url = new URL(`http://${this.host}/api.cgi`);
const params = url.searchParams;
params.set('cmd', 'AudioAlarmPlay');
params.set('user', this.username);
params.set('password', this.password);
const createReadable = (data: any) => {
const pt = new PassThrough();
pt.write(Buffer.from(JSON.stringify(data)));
Expand All @@ -296,7 +339,7 @@ export class ReolinkCameraClient {
};
}

const response = await this.request({
const response = await this.requestWithLogin({
url,
method: 'POST',
responseType: 'json',
Expand Down

0 comments on commit fc93a85

Please sign in to comment.