378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import { logger } from './unifi.logger.js';
|
|
import { UnifiHttp } from './classes.unifihttp.js';
|
|
import { DeviceManager } from './classes.devicemanager.js';
|
|
import { ClientManager } from './classes.clientmanager.js';
|
|
import type {
|
|
IUnifiControllerOptions,
|
|
INetworkAuthResponse,
|
|
INetworkSite,
|
|
IUnifiApiResponse,
|
|
THttpMethod,
|
|
} from './interfaces/index.js';
|
|
|
|
/**
|
|
* UniFi Controller - Entry point for Network Controller API
|
|
*
|
|
* This class provides access to the UniFi Network Controller API.
|
|
* Supports two authentication methods:
|
|
* 1. API Key (preferred) - Set X-API-Key header, no login required
|
|
* 2. Session auth - Username/password login with session cookies
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Using API key (no login required)
|
|
* const controller = new UnifiController({
|
|
* host: '192.168.1.1',
|
|
* apiKey: 'your-api-key',
|
|
* });
|
|
* const devices = await controller.deviceManager.listDevices();
|
|
*
|
|
* // Using session auth (requires login)
|
|
* const controller = new UnifiController({
|
|
* host: '192.168.1.1',
|
|
* username: 'admin',
|
|
* password: 'password',
|
|
* });
|
|
* await controller.login();
|
|
* const devices = await controller.deviceManager.listDevices();
|
|
* await controller.logout();
|
|
* ```
|
|
*/
|
|
export class UnifiController {
|
|
/** Controller host */
|
|
private host: string;
|
|
|
|
/** API key for authentication */
|
|
private apiKey?: string;
|
|
|
|
/** Username for session authentication */
|
|
private username?: string;
|
|
|
|
/** Password for session authentication */
|
|
private password?: string;
|
|
|
|
/** Controller type determines API paths */
|
|
private controllerType: 'unifi-os' | 'udm-pro' | 'standalone';
|
|
|
|
/** Whether to verify SSL certificates */
|
|
private verifySsl: boolean;
|
|
|
|
/** HTTP client */
|
|
private http: UnifiHttp;
|
|
|
|
/** Whether currently authenticated */
|
|
private authenticated: boolean = false;
|
|
|
|
/** CSRF token (for UniFi OS session auth) */
|
|
private csrfToken?: string;
|
|
|
|
/** Device manager instance */
|
|
public deviceManager: DeviceManager;
|
|
|
|
/** Client manager instance */
|
|
public clientManager: ClientManager;
|
|
|
|
constructor(options: IUnifiControllerOptions) {
|
|
this.host = options.host.replace(/\/$/, '');
|
|
this.apiKey = options.apiKey;
|
|
this.username = options.username;
|
|
this.password = options.password;
|
|
this.controllerType = options.controllerType || 'unifi-os';
|
|
this.verifySsl = options.verifySsl ?? false;
|
|
|
|
// Build base URL based on controller type
|
|
const baseUrl = this.getBaseUrl();
|
|
this.http = new UnifiHttp(baseUrl, this.verifySsl);
|
|
|
|
// If API key provided, set it and mark as authenticated
|
|
if (this.apiKey) {
|
|
this.http.setHeader('X-API-Key', this.apiKey);
|
|
this.authenticated = true;
|
|
}
|
|
|
|
// Initialize managers
|
|
this.deviceManager = new DeviceManager(this);
|
|
this.clientManager = new ClientManager(this);
|
|
|
|
logger.log('info', `UnifiController initialized for ${this.host} (${this.controllerType})`);
|
|
}
|
|
|
|
/**
|
|
* Get the base URL for API requests based on controller type
|
|
*/
|
|
private getBaseUrl(): string {
|
|
// Add https:// if not present
|
|
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
|
|
|
switch (this.controllerType) {
|
|
case 'unifi-os':
|
|
case 'udm-pro':
|
|
// UniFi OS consoles (UDM, UDM Pro, Cloud Key Gen2+) use /proxy/network prefix
|
|
return `${host}/proxy/network`;
|
|
case 'standalone':
|
|
// Standalone controllers (software controller)
|
|
return host;
|
|
default:
|
|
return host;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the login endpoint based on controller type
|
|
*/
|
|
private getLoginEndpoint(): string {
|
|
switch (this.controllerType) {
|
|
case 'unifi-os':
|
|
case 'udm-pro':
|
|
// UniFi OS uses the root /api/auth/login
|
|
return '/api/auth/login';
|
|
case 'standalone':
|
|
return '/api/login';
|
|
default:
|
|
return '/api/login';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the logout endpoint based on controller type
|
|
*/
|
|
private getLogoutEndpoint(): string {
|
|
switch (this.controllerType) {
|
|
case 'unifi-os':
|
|
case 'udm-pro':
|
|
return '/api/auth/logout';
|
|
case 'standalone':
|
|
return '/api/logout';
|
|
default:
|
|
return '/api/logout';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login to the controller (only needed for session auth, not API key)
|
|
*/
|
|
public async login(): Promise<void> {
|
|
// If using API key, already authenticated
|
|
if (this.apiKey) {
|
|
logger.log('info', 'Using API key authentication, no login required');
|
|
this.authenticated = true;
|
|
return;
|
|
}
|
|
|
|
if (!this.username || !this.password) {
|
|
throw new Error('Username and password required for session authentication');
|
|
}
|
|
|
|
logger.log('info', `Logging in to UniFi Controller at ${this.host}`);
|
|
|
|
// For UniFi OS, login happens at the console level (not through /proxy/network)
|
|
let loginUrl: string;
|
|
if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') {
|
|
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
|
loginUrl = `${host}${this.getLoginEndpoint()}`;
|
|
} else {
|
|
loginUrl = `${this.getBaseUrl()}${this.getLoginEndpoint()}`;
|
|
}
|
|
|
|
// Create a separate HTTP client for login (different base URL for UniFi OS)
|
|
const loginHttp = new UnifiHttp(
|
|
loginUrl.replace(this.getLoginEndpoint(), ''),
|
|
this.verifySsl
|
|
);
|
|
|
|
const response = await loginHttp.rawRequest('POST', this.getLoginEndpoint(), {
|
|
username: this.username,
|
|
password: this.password,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
throw new Error(`Login failed: HTTP ${response.status} - ${errorText}`);
|
|
}
|
|
|
|
// Store cookies from response
|
|
const setCookieHeaders = response.headers?.['set-cookie'];
|
|
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
|
|
this.http.setCookies(setCookieHeaders);
|
|
}
|
|
|
|
// Extract CSRF token from response (UniFi OS)
|
|
const csrfHeader = response.headers?.['x-csrf-token'];
|
|
if (csrfHeader) {
|
|
this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader;
|
|
this.http.setHeader('X-CSRF-Token', this.csrfToken);
|
|
}
|
|
|
|
this.authenticated = true;
|
|
logger.log('info', 'Successfully logged in to UniFi Controller');
|
|
}
|
|
|
|
/**
|
|
* Logout from the controller (only for session auth)
|
|
*/
|
|
public async logout(): Promise<void> {
|
|
// If using API key, nothing to logout
|
|
if (this.apiKey) {
|
|
return;
|
|
}
|
|
|
|
if (!this.authenticated) {
|
|
return;
|
|
}
|
|
|
|
logger.log('info', 'Logging out from UniFi Controller');
|
|
|
|
try {
|
|
// For UniFi OS, logout happens at the console level
|
|
if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') {
|
|
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
|
|
const logoutHttp = new UnifiHttp(host, this.verifySsl);
|
|
logoutHttp.setCookies([this.http.getCookieHeader()]);
|
|
if (this.csrfToken) {
|
|
logoutHttp.setHeader('X-CSRF-Token', this.csrfToken);
|
|
}
|
|
await logoutHttp.rawRequest('POST', this.getLogoutEndpoint());
|
|
} else {
|
|
await this.http.rawRequest('POST', this.getLogoutEndpoint());
|
|
}
|
|
} catch (error) {
|
|
logger.log('warn', `Logout error (may be expected): ${error}`);
|
|
}
|
|
|
|
this.authenticated = false;
|
|
this.csrfToken = undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if logged in
|
|
*/
|
|
public isAuthenticated(): boolean {
|
|
return this.authenticated;
|
|
}
|
|
|
|
/**
|
|
* Make a request to the controller API
|
|
*/
|
|
public async request<T>(
|
|
method: THttpMethod,
|
|
endpoint: string,
|
|
data?: unknown
|
|
): Promise<T> {
|
|
if (!this.authenticated) {
|
|
throw new Error('Not authenticated. Call login() first or provide API key.');
|
|
}
|
|
|
|
return this.http.request<T>(method, endpoint, data);
|
|
}
|
|
|
|
/**
|
|
* List all sites on this controller
|
|
*/
|
|
public async listSites(): Promise<INetworkSite[]> {
|
|
logger.log('debug', 'Fetching sites');
|
|
|
|
const response = await this.request<IUnifiApiResponse<INetworkSite>>(
|
|
'GET',
|
|
'/api/self/sites'
|
|
);
|
|
|
|
return response.data || [];
|
|
}
|
|
|
|
/**
|
|
* Get system info
|
|
*/
|
|
public async getSystemInfo(): Promise<unknown> {
|
|
return this.request('GET', '/api/s/default/stat/sysinfo');
|
|
}
|
|
|
|
/**
|
|
* Get controller health
|
|
*/
|
|
public async getHealth(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/stat/health`);
|
|
}
|
|
|
|
/**
|
|
* Get active alerts
|
|
*/
|
|
public async getAlerts(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/stat/alarm`);
|
|
}
|
|
|
|
/**
|
|
* Get events
|
|
*/
|
|
public async getEvents(
|
|
siteId: string = 'default',
|
|
options: { start?: number; end?: number; limit?: number } = {}
|
|
): Promise<unknown> {
|
|
const params = new URLSearchParams();
|
|
if (options.start) params.append('start', options.start.toString());
|
|
if (options.end) params.append('end', options.end.toString());
|
|
if (options.limit) params.append('_limit', options.limit.toString());
|
|
|
|
const queryString = params.toString() ? `?${params.toString()}` : '';
|
|
return this.request('GET', `/api/s/${siteId}/stat/event${queryString}`);
|
|
}
|
|
|
|
/**
|
|
* Get WLAN configurations
|
|
*/
|
|
public async getWlans(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/rest/wlanconf`);
|
|
}
|
|
|
|
/**
|
|
* Get network configurations
|
|
*/
|
|
public async getNetworks(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/rest/networkconf`);
|
|
}
|
|
|
|
/**
|
|
* Get port forward rules
|
|
*/
|
|
public async getPortForwards(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/rest/portforward`);
|
|
}
|
|
|
|
/**
|
|
* Get firewall rules
|
|
*/
|
|
public async getFirewallRules(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/rest/firewallrule`);
|
|
}
|
|
|
|
/**
|
|
* Get DPI stats
|
|
*/
|
|
public async getDpiStats(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('GET', `/api/s/${siteId}/stat/dpi`);
|
|
}
|
|
|
|
/**
|
|
* Backup the controller configuration
|
|
*/
|
|
public async createBackup(siteId: string = 'default'): Promise<unknown> {
|
|
return this.request('POST', `/api/s/${siteId}/cmd/backup`, {
|
|
cmd: 'backup',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get devices (convenience method)
|
|
*/
|
|
public async getDevices(siteId: string = 'default') {
|
|
return this.deviceManager.listDevices(siteId);
|
|
}
|
|
|
|
/**
|
|
* Get active clients (convenience method)
|
|
*/
|
|
public async getClients(siteId: string = 'default') {
|
|
return this.clientManager.listActiveClients(siteId);
|
|
}
|
|
}
|