114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
|
|
import * as http from 'node:http';
|
||
|
|
import { URL } from 'node:url';
|
||
|
|
import { logger } from './logger.ts';
|
||
|
|
import type { IUpsStatus } from './daemon.ts';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* HTTP Server for exposing UPS status as JSON
|
||
|
|
* Serves cached data from the daemon's monitoring loop
|
||
|
|
*/
|
||
|
|
export class NupstHttpServer {
|
||
|
|
private server?: http.Server;
|
||
|
|
private port: number;
|
||
|
|
private path: string;
|
||
|
|
private authToken: string;
|
||
|
|
private getUpsStatus: () => Map<string, IUpsStatus>;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Create a new HTTP server instance
|
||
|
|
* @param port Port to listen on
|
||
|
|
* @param path URL path for the endpoint
|
||
|
|
* @param authToken Authentication token required for access
|
||
|
|
* @param getUpsStatus Function to retrieve cached UPS status
|
||
|
|
*/
|
||
|
|
constructor(
|
||
|
|
port: number,
|
||
|
|
path: string,
|
||
|
|
authToken: string,
|
||
|
|
getUpsStatus: () => Map<string, IUpsStatus>
|
||
|
|
) {
|
||
|
|
this.port = port;
|
||
|
|
this.path = path;
|
||
|
|
this.authToken = authToken;
|
||
|
|
this.getUpsStatus = getUpsStatus;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Verify authentication token from request
|
||
|
|
* Supports both Bearer token in Authorization header and token query parameter
|
||
|
|
* @param req HTTP request
|
||
|
|
* @returns True if authenticated, false otherwise
|
||
|
|
*/
|
||
|
|
private isAuthenticated(req: http.IncomingMessage): boolean {
|
||
|
|
// Check Authorization header (Bearer token)
|
||
|
|
const authHeader = req.headers.authorization;
|
||
|
|
if (authHeader?.startsWith('Bearer ')) {
|
||
|
|
const token = authHeader.substring(7);
|
||
|
|
return token === this.authToken;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check token query parameter
|
||
|
|
if (req.url) {
|
||
|
|
const url = new URL(req.url, `http://localhost:${this.port}`);
|
||
|
|
const tokenParam = url.searchParams.get('token');
|
||
|
|
return tokenParam === this.authToken;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start the HTTP server
|
||
|
|
*/
|
||
|
|
public start(): void {
|
||
|
|
this.server = http.createServer((req, res) => {
|
||
|
|
// Parse URL
|
||
|
|
const reqUrl = new URL(req.url || '/', `http://localhost:${this.port}`);
|
||
|
|
|
||
|
|
if (reqUrl.pathname === this.path && req.method === 'GET') {
|
||
|
|
// Check authentication
|
||
|
|
if (!this.isAuthenticated(req)) {
|
||
|
|
res.writeHead(401, {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'WWW-Authenticate': 'Bearer'
|
||
|
|
});
|
||
|
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get cached status (no refresh)
|
||
|
|
const statusMap = this.getUpsStatus();
|
||
|
|
const statusArray = Array.from(statusMap.values());
|
||
|
|
|
||
|
|
res.writeHead(200, {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Cache-Control': 'no-cache'
|
||
|
|
});
|
||
|
|
res.end(JSON.stringify(statusArray, null, 2));
|
||
|
|
} else {
|
||
|
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||
|
|
res.end(JSON.stringify({ error: 'Not Found' }));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.server.listen(this.port, () => {
|
||
|
|
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
this.server.on('error', (error: any) => {
|
||
|
|
logger.error(`HTTP server error: ${error.message}`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stop the HTTP server
|
||
|
|
*/
|
||
|
|
public stop(): void {
|
||
|
|
if (this.server) {
|
||
|
|
this.server.close(() => {
|
||
|
|
logger.log('HTTP server stopped');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|