Files
unifi/ts/classes.unifi-controller.ts

378 lines
11 KiB
TypeScript
Raw Permalink Normal View History

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);
}
}