feat(unifi): implement comprehensive UniFi API client with controllers, protect, access, account, managers, resources, HTTP client, interfaces, logging, plugins, and tests
This commit is contained in:
265
ts/classes.unifihttp.ts
Normal file
265
ts/classes.unifihttp.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user