266 lines
7.2 KiB
TypeScript
266 lines
7.2 KiB
TypeScript
|
|
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);
|
||
|
|
}
|
||
|
|
}
|