Files
scrypted/plugins/hikvision-doorbell/src/auth-request.ts
Roman Sokolov 3d1d3727dc hikvision-doorbell: fixes (#1970)
* Let's try to fix the plugin freezing

* hikvision-doorbell version up after merging from main
2026-01-24 08:18:59 -08:00

225 lines
6.7 KiB
TypeScript

import Http from 'http';
import { HttpFetchResponseType } from '@scrypted/common/src/http-auth-fetch';
import { HttpFetchResponse } from '@scrypted/server/src/fetch';
import { Readable } from 'stream';
import * as Auth from 'http-auth-client';
export interface AuthRequestOptions extends Http.RequestOptions {
sessionAuth?: Auth.Basic | Auth.Digest | Auth.Bearer;
responseType: HttpFetchResponseType;
// Internal: number of digest retries performed for this request
digestRetry?: number;
}
export type AuthRequestBody = string | Buffer | Readable;
export class AuthRequst {
private username: string;
private password: string;
private console: Console;
private auth: Auth.Basic | Auth.Digest | Auth.Bearer;
constructor(username:string, password: string, console: Console) {
this.username = username;
this.password = password;
this.console = console;
}
async request(url: string, options: AuthRequestOptions, body?: AuthRequestBody) {
let opt = {...options};
if (typeof opt.method === 'undefined') {
opt.method = 'GET';
}
if (opt.headers === undefined) {
delete opt.headers;
}
const response = new Promise<HttpFetchResponse<any>>( (resolve, reject) => {
const req = Http.request(url, opt)
// Apply timeout if specified (Node.js http.request doesn't use timeout from options)
if (opt.timeout) {
req.setTimeout (opt.timeout, () => {
req.destroy (new Error (`Request timeout after ${opt.timeout}ms`));
});
}
req.once('response', async (resp) => {
try {
if (resp.statusCode == 401) {
// Hikvision quirk: even if we already had a sessionAuth, a fresh
// WWW-Authenticate challenge may require rebuilding credentials.
// Limit the number of digest rebuilds to avoid infinite loops.
const attempt = (opt.digestRetry ?? 0);
if (attempt >= 2) {
// Give up after a couple of rebuild attempts and surface the 401 response
resolve(await this.parseResponse (opt.responseType, resp));
return;
}
const newAuth = this.createAuth(resp.headers['www-authenticate'], !!this.auth);
// Clear cached auth to avoid stale nonce reuse
this.auth = undefined;
opt.sessionAuth = newAuth;
opt.digestRetry = attempt + 1;
const result = await this.request(url, opt, body);
resolve(result);
}
else {
// Cache the negotiated session auth only if it was provided for this request.
if (opt.sessionAuth) {
this.auth = opt.sessionAuth;
}
resolve(await this.parseResponse(opt.responseType, resp));
}
} catch (error) {
reject(error);
}
});
req.once('error', (error) => {
reject(error);
});
if (opt.sessionAuth) {
req.setHeader('Authorization', opt.sessionAuth.authorization(req.method, req.path));
}
else if (this.auth) {
req.setHeader('Authorization', this.auth.authorization(req.method, req.path));
}
if (typeof body === 'undefined') {
req.end();
}
else {
this.readableBody(req, body).pipe(req);
req.flushHeaders();
}
});
return response;
}
private createAuth(authenticate: string, noThrow: boolean) {
try {
const challenges = Auth.parseHeaders(authenticate);
let auth = Auth.create(challenges);
return auth.credentials(this.username, this.password);
}
catch (error) {
if (noThrow) {
return undefined;
}
throw error;
}
}
private async parseResponse(responseType: HttpFetchResponseType, msg: Http.IncomingMessage): Promise<HttpFetchResponse<any>> {
let body: any;
switch (responseType) {
case 'json':
const text = await this.readText(msg);
body = JSON.parse(text);
break;
case 'text':
body = await this.readText(msg);
break;
case 'buffer':
body = await this.readBuffer(msg);
break;
default:
body = msg;
}
const incomingHeaders = new Headers();
for (const [k, v] of Object.entries(msg.headers)) {
for (const vv of (typeof v === 'string' ? [v] : v)) {
incomingHeaders.append(k, vv)
}
}
let result: HttpFetchResponse<any> = {
body,
headers: incomingHeaders,
statusCode: msg.statusCode
};
return result;
}
private async readText(readable: Readable): Promise<string> {
let result: string = '';
return new Promise<string>((resolve, reject) => {
readable.setEncoding('utf-8');
readable.on('data', chunk => {
result += chunk;
});
readable.once('end', () => {
resolve(result);
});
readable.once('error', (error) => {
reject(error);
});
});
}
private async readBuffer(readable: Readable): Promise<Buffer> {
let result = Buffer.alloc(0);
return new Promise<Buffer>((resolve, reject) => {
readable.on('data', chunk => {
result = Buffer.concat([result, chunk]);
});
readable.once('end', () => {
resolve(result);
});
readable.once('error', (error) => {
reject(error);
});
});
}
private readableBody(requst: Http.ClientRequest, body: AuthRequestBody): Readable {
if (typeof body === 'string') {
body = Buffer.from(body);
}
if (body instanceof Buffer) {
const len = body.byteLength;
requst.setHeader('Content-Type', 'application/octet-stream');
requst.setHeader('Content-Length', len);
body = Readable.from(body);
}
return body;
}
}