Files
unifi/ts/classes.unifihttp.ts

266 lines
7.2 KiB
TypeScript
Raw Normal View History

import * as plugins from './plugins.js';
import { logger } from './unifi.logger.js';
import type { THttpMethod } from './interfaces/index.js';
/**
* Extended response type
*/
export interface IUnifiHttpResponse<T = any> {
ok: boolean;
status: number;
statusText?: string;
headers?: Record<string, string | string[]>;
body: T;
text: () => Promise<string>;
json: () => Promise<T>;
}
/**
* Base HTTP client for UniFi APIs with SSL handling and authentication support
*/
export class UnifiHttp {
private baseUrl: string;
private headers: Record<string, string> = {};
private verifySsl: boolean;
private cookies: string[] = [];
constructor(baseUrl: string, verifySsl: boolean = false) {
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.verifySsl = verifySsl;
}
/**
* Set a header value
*/
public setHeader(name: string, value: string): void {
this.headers[name] = value;
}
/**
* Remove a header
*/
public removeHeader(name: string): void {
delete this.headers[name];
}
/**
* Set cookies from Set-Cookie headers
*/
public setCookies(cookies: string[]): void {
this.cookies = cookies;
}
/**
* Get the cookie header string
*/
public getCookieHeader(): string {
return this.cookies
.map((cookie) => cookie.split(';')[0])
.join('; ');
}
/**
* Build request with common options
*/
private buildRequest(url: string, data?: unknown): plugins.smartrequest.SmartRequestClient {
let requestBuilder = plugins.smartrequest.SmartRequestClient.create()
.url(url)
.header('Content-Type', 'application/json');
// Add stored headers
for (const [name, value] of Object.entries(this.headers)) {
requestBuilder = requestBuilder.header(name, value);
}
// Add cookies
const cookieHeader = this.getCookieHeader();
if (cookieHeader) {
requestBuilder = requestBuilder.header('Cookie', cookieHeader);
}
// Add JSON data
if (data) {
requestBuilder = requestBuilder.json(data);
}
return requestBuilder;
}
/**
* Make an HTTP request
*/
public async request<T>(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
logger.log('debug', `UniFi HTTP ${method} ${url}`);
const requestBuilder = this.buildRequest(url, data);
let response: plugins.smartrequest.IExtendedIncomingMessage;
// Note: smartrequest v2 doesn't have built-in SSL bypass, but Node's default is fine for most cases
// For self-signed certs, we need to handle at the environment level or use NODE_TLS_REJECT_UNAUTHORIZED
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
if (!this.verifySsl) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
try {
switch (method) {
case 'GET':
response = await requestBuilder.get();
break;
case 'POST':
response = await requestBuilder.post();
break;
case 'PUT':
response = await requestBuilder.put();
break;
case 'DELETE':
response = await requestBuilder.delete();
break;
case 'PATCH':
response = await requestBuilder.patch();
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
} finally {
if (!this.verifySsl) {
if (originalRejectUnauthorized !== undefined) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
} else {
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
}
}
}
// Store cookies from response
const setCookieHeaders = response.headers?.['set-cookie'];
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
this.setCookies(setCookieHeaders);
}
// Check for HTTP errors
const statusCode = response.statusCode || 0;
if (statusCode >= 400) {
const errorBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body);
logger.log('error', `UniFi HTTP error: ${statusCode} - ${errorBody}`);
throw new Error(`HTTP ${statusCode}: ${response.statusMessage || 'Unknown'} - ${errorBody}`);
}
// Return body directly
return response.body as T;
}
/**
* Make a raw request returning the full response (for handling cookies/headers)
*/
public async rawRequest(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<IUnifiHttpResponse> {
const url = `${this.baseUrl}${endpoint}`;
logger.log('debug', `UniFi HTTP raw ${method} ${url}`);
const requestBuilder = this.buildRequest(url, data);
let response: plugins.smartrequest.IExtendedIncomingMessage;
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
if (!this.verifySsl) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
try {
switch (method) {
case 'GET':
response = await requestBuilder.get();
break;
case 'POST':
response = await requestBuilder.post();
break;
case 'PUT':
response = await requestBuilder.put();
break;
case 'DELETE':
response = await requestBuilder.delete();
break;
case 'PATCH':
response = await requestBuilder.patch();
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
} finally {
if (!this.verifySsl) {
if (originalRejectUnauthorized !== undefined) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
} else {
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
}
}
}
// Store cookies from response
const setCookieHeaders = response.headers?.['set-cookie'];
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
this.setCookies(setCookieHeaders);
}
const statusCode = response.statusCode || 0;
const body = response.body;
return {
ok: statusCode >= 200 && statusCode < 300,
status: statusCode,
statusText: response.statusMessage,
headers: response.headers as Record<string, string | string[]>,
body,
text: async () => typeof body === 'string' ? body : JSON.stringify(body),
json: async () => typeof body === 'object' ? body : JSON.parse(body as string),
};
}
/**
* Shorthand for GET requests
*/
public async get<T>(endpoint: string): Promise<T> {
return this.request<T>('GET', endpoint);
}
/**
* Shorthand for POST requests
*/
public async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>('POST', endpoint, data);
}
/**
* Shorthand for PUT requests
*/
public async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>('PUT', endpoint, data);
}
/**
* Shorthand for DELETE requests
*/
public async delete<T>(endpoint: string): Promise<T> {
return this.request<T>('DELETE', endpoint);
}
/**
* Shorthand for PATCH requests
*/
public async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>('PATCH', endpoint, data);
}
}