89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
|
|
// API Proxy Handler - proxies REST requests to desktop apps
|
||
|
|
|
||
|
|
import { Request, Response, NextFunction } from 'express';
|
||
|
|
import { ConnectionManager } from '../services/ConnectionManager';
|
||
|
|
import { SessionManager } from '../services/SessionManager';
|
||
|
|
import { logger } from '../utils/logger';
|
||
|
|
|
||
|
|
export function createApiProxy(
|
||
|
|
connectionManager: ConnectionManager,
|
||
|
|
sessionManager: SessionManager
|
||
|
|
) {
|
||
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||
|
|
const sessionId = req.params.sessionId;
|
||
|
|
|
||
|
|
// Check session exists
|
||
|
|
const session = sessionManager.getSession(sessionId);
|
||
|
|
if (!session) {
|
||
|
|
return res.status(404).json({ error: 'Session not found' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check password in header or query
|
||
|
|
const password = req.headers['x-macropad-password'] as string ||
|
||
|
|
req.query.password as string;
|
||
|
|
|
||
|
|
if (!password) {
|
||
|
|
return res.status(401).json({ error: 'Password required' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const valid = await sessionManager.validatePassword(sessionId, password);
|
||
|
|
if (!valid) {
|
||
|
|
return res.status(401).json({ error: 'Invalid password' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check desktop is connected
|
||
|
|
const desktop = connectionManager.getDesktopBySessionId(sessionId);
|
||
|
|
if (!desktop) {
|
||
|
|
return res.status(503).json({ error: 'Desktop not connected' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Extract the API path (remove the session ID prefix)
|
||
|
|
const apiPath = req.path.replace(`/${sessionId}`, '');
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await connectionManager.forwardApiRequest(
|
||
|
|
sessionId,
|
||
|
|
req.method,
|
||
|
|
apiPath,
|
||
|
|
req.body,
|
||
|
|
filterHeaders(req.headers)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Handle binary responses (images)
|
||
|
|
if (response.body?.base64 && response.body?.contentType) {
|
||
|
|
const buffer = Buffer.from(response.body.base64, 'base64');
|
||
|
|
res.set('Content-Type', response.body.contentType);
|
||
|
|
res.send(buffer);
|
||
|
|
} else {
|
||
|
|
res.status(response.status).json(response.body);
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error(`API proxy error for ${sessionId}:`, error);
|
||
|
|
|
||
|
|
if (error.message === 'Request timeout') {
|
||
|
|
return res.status(504).json({ error: 'Desktop request timeout' });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (error.message === 'Desktop not connected' || error.message === 'Desktop disconnected') {
|
||
|
|
return res.status(503).json({ error: 'Desktop not connected' });
|
||
|
|
}
|
||
|
|
|
||
|
|
res.status(500).json({ error: 'Proxy error' });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterHeaders(headers: any): Record<string, string> {
|
||
|
|
// Only forward relevant headers
|
||
|
|
const allowed = ['content-type', 'accept', 'accept-language'];
|
||
|
|
const filtered: Record<string, string> = {};
|
||
|
|
|
||
|
|
for (const key of allowed) {
|
||
|
|
if (headers[key]) {
|
||
|
|
filtered[key] = headers[key];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return filtered;
|
||
|
|
}
|