homebridge-unifi-ap-light
Version:
Control the blue LED ring on your UniFi APs!
166 lines • 6.36 kB
JavaScript
import Axios from 'axios';
import jwt from 'jsonwebtoken';
import https from 'https';
import { parse as parseCookie } from 'cookie';
/**
* Manages authentication sessions and re-authentication.
*/
export class SessionManager {
constructor(host, username, password, log) {
this.axiosInstance = null;
this.siteMap = new Map();
this.isUniFiOS = false;
this.host = host;
this.username = username;
this.password = password;
this.log = log;
}
/**
* Authenticates using both methods in parallel, and adopts the first one that succeeds.
*/
async authenticate() {
this.log.debug('Starting parallel authentication attempts...');
const primaryAttempt = this.tryPrimaryAuth();
const secondaryAttempt = this.trySecondaryAuth();
try {
const { instance, isUniFiOS } = await Promise.any([primaryAttempt, secondaryAttempt]);
// Assign the working Axios instance and detected mode
this.axiosInstance = instance;
this.isUniFiOS = isUniFiOS;
this.log.debug(`Authentication successful. UniFi OS mode: ${isUniFiOS}`);
await this.loadSites();
}
catch (error) {
this.log.error(`Both authentication methods failed: ${error}`);
throw error;
}
}
/**
* Auth using /api/login for self-hosted consoles.
* Returns a fully-configured Axios instance.
*/
async tryPrimaryAuth() {
const instance = Axios.create({
baseURL: `https://${this.host}`,
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
});
this.log.debug('Trying primary (self-hosted) authentication...');
const response = await instance.post('/api/login', {
username: this.username,
password: this.password,
});
if (!response.headers['set-cookie']) {
throw new Error('Primary auth failed: No cookies returned');
}
instance.defaults.headers.common['Cookie'] = response.headers['set-cookie'].join('; ');
return { instance, isUniFiOS: false };
}
/**
* Auth using /api/auth/login for UniFi OS consoles.
* Returns a fully-configured Axios instance.
*/
async trySecondaryAuth() {
var _a;
const instance = Axios.create({
baseURL: `https://${this.host}`,
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
});
this.log.debug('Trying secondary (UniFi OS) authentication...');
const response = await instance.post('/api/auth/login', {
username: this.username,
password: this.password,
rememberMe: true,
});
const setCookie = response.headers['set-cookie'];
if (!setCookie) {
throw new Error('Secondary auth failed: No cookies returned');
}
let token = '';
let csrfToken = '';
try {
const parsed = parseCookie(setCookie.join('; '));
token = (_a = parsed['TOKEN']) !== null && _a !== void 0 ? _a : '';
const decoded = jwt.decode(token);
csrfToken = decoded === null || decoded === void 0 ? void 0 : decoded.csrfToken;
}
catch (err) {
this.log.error(`Cookie parsing or token decoding failed: ${err}`);
throw new Error('Secondary authentication failed: Malformed cookie or token.');
}
if (!csrfToken) {
throw new Error('Secondary auth failed: CSRF token not found.');
}
instance.defaults.headers.common['X-Csrf-Token'] = csrfToken;
instance.defaults.headers.common['Cookie'] = `${setCookie.join('; ')}; TOKEN=${token}`;
return { instance, isUniFiOS: true };
}
/**
* Handles API requests with retry on 401
*/
async request(config) {
var _a;
if (!this.axiosInstance) {
throw new Error('Cannot make API request: No authenticated session.');
}
try {
return await this.axiosInstance(config);
}
catch (error) {
const axiosError = error;
if (((_a = axiosError.response) === null || _a === void 0 ? void 0 : _a.status) === 401) {
this.log.warn('Session expired, retrying authentication...');
await this.authenticate();
return await this.axiosInstance(config);
}
else {
throw axiosError;
}
}
}
/**
* Load available sites from the appropriate endpoint.
*/
async loadSites() {
var _a;
const url = this.isUniFiOS ? '/proxy/network/api/self/sites' : '/api/self/sites';
try {
const response = await this.request({ url, method: 'get' });
const sites = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.data;
if (Array.isArray(sites)) {
for (const site of sites) {
if (site.desc)
this.siteMap.set(site.desc, site.name);
if (site.name)
this.siteMap.set(site.name, site.name);
}
this.log.debug(`Loaded sites from ${url}: ${Array.from(this.siteMap.keys()).join(', ')}`);
}
else {
throw new Error('Unexpected site list structure');
}
}
catch (error) {
this.log.error(`Failed to load site list from ${url}: ${error}`);
throw error;
}
}
getSiteName(friendlyName) {
const internal = this.siteMap.get(friendlyName);
if (!internal) {
this.log.warn(`Configured site "${friendlyName}" not recognized. Available: ${this.getAvailableSitePairs().join(', ')}`);
}
return internal;
}
getAvailableSitePairs() {
const seen = new Set();
const pairs = [];
for (const [key, value] of this.siteMap.entries()) {
if (key !== value && !seen.has(value)) {
pairs.push(`${key} (${value})`);
seen.add(value);
}
}
return pairs;
}
}
//# sourceMappingURL=sessionManager.js.map