mirror of
https://github.com/koush/scrypted.git
synced 2026-02-03 06:03:27 +00:00
* Let's try to fix the plugin freezing * hikvision-doorbell version up after merging from main
225 lines
6.7 KiB
TypeScript
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;
|
|
}
|
|
}
|