153 lines
4.8 KiB
JavaScript
153 lines
4.8 KiB
JavaScript
import http from 'http';
|
|
import process from 'process';
|
|
import { config } from '../../internal/config/config.js';
|
|
import { logger } from '../../internal/logger/logger.js';
|
|
import { RateLimiter } from '../../internal/middleware/ratelimit.js';
|
|
import { Store } from '../../internal/session/store.js';
|
|
import { Hub } from '../../internal/ws/hub.js';
|
|
import { setupWebSocket } from '../../internal/ws/handler.js';
|
|
import { startTimer } from '../../internal/timer/timer.js';
|
|
import { initializeDatabase, logSessionCreated } from '../../internal/db/db.js';
|
|
|
|
// Bootstrapping log
|
|
logger.info('server_initiating', {
|
|
addr: config.serverAddr,
|
|
environment: config.environment
|
|
});
|
|
|
|
const store = new Store();
|
|
const hub = new Hub(store);
|
|
const limiter = new RateLimiter(config.rateLimitPerMin, 60000); // 1 minute window
|
|
|
|
// Configure standard HTTP server to manage REST actions
|
|
const server = http.createServer((req, res) => {
|
|
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
|
|
// Endpoint: GET /health
|
|
if (url.pathname === '/health' && req.method === 'GET') {
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
status: 'ok',
|
|
active_sessions: store.getActiveCount(),
|
|
connected_clients: hub.getConnectedClientsCount()
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Endpoint: POST /sessions
|
|
// Pre-creates call session mapping. Authenticated using X-API-Key header.
|
|
if (url.pathname === '/sessions' && req.method === 'POST') {
|
|
const apiKeyHeader = req.headers['x-api-key'];
|
|
if (!apiKeyHeader || apiKeyHeader !== config.apiKey) {
|
|
logger.warn('unauthorized_session_creation_attempt', {
|
|
remote_ip: req.socket.remoteAddress
|
|
});
|
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
return;
|
|
}
|
|
|
|
let body = '';
|
|
req.on('data', chunk => {
|
|
body += chunk;
|
|
});
|
|
|
|
req.on('end', async () => {
|
|
let params;
|
|
try {
|
|
params = JSON.parse(body);
|
|
} catch (err) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'invalid_json' }));
|
|
return;
|
|
}
|
|
|
|
const { ride_id, driver_id, passenger_id, driver_ip, passenger_ip } = params;
|
|
if (!ride_id || !driver_id || !passenger_id) {
|
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'missing_parameters' }));
|
|
return;
|
|
}
|
|
|
|
let sess;
|
|
try {
|
|
// Enforce hard 60s maximum call duration
|
|
sess = store.createSession(ride_id, driver_id, passenger_id, 60000, driver_ip, passenger_ip);
|
|
} catch (err) {
|
|
logger.warn('session_creation_failed', { ride_id, error: err.message });
|
|
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'session_exists' }));
|
|
return;
|
|
}
|
|
|
|
// Start 60s timeout countdown
|
|
sess.timer = startTimer(60000, () => {
|
|
hub.forceEndSession(sess.sessionID, 'max_duration_reached');
|
|
});
|
|
|
|
// Log session creation to database
|
|
await logSessionCreated(sess.sessionID, sess.rideID, sess.driverID, sess.passengerID, sess.driverIP, sess.passengerIP);
|
|
|
|
logger.info('session_created', {
|
|
session_id: sess.sessionID,
|
|
ride_id: sess.rideID,
|
|
driver_id: sess.driverID,
|
|
passenger_id: sess.passengerID,
|
|
driver_ip: sess.driverIP,
|
|
passenger_ip: sess.passengerIP
|
|
});
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
session_id: sess.sessionID,
|
|
ride_id: sess.rideID,
|
|
expires_in: 60
|
|
}));
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
res.end('Not Found');
|
|
});
|
|
|
|
// Bind WebSocket upgrades interceptor
|
|
setupWebSocket(server, hub, limiter);
|
|
|
|
// Resolve address binding parts
|
|
const [host, portStr] = config.serverAddr.split(':');
|
|
const port = parseInt(portStr, 10);
|
|
|
|
// Initialize database first then start server
|
|
initializeDatabase().then(() => {
|
|
server.listen(port, host, () => {
|
|
logger.info('server_running', { addr: config.serverAddr });
|
|
});
|
|
});
|
|
|
|
// Graceful exit handling
|
|
function shutdown(signal) {
|
|
logger.info('server_stopping', { signal });
|
|
|
|
// Close HTTP server to stop accepting new traffic
|
|
server.close((err) => {
|
|
if (err) {
|
|
logger.error('server_close_error', { error: err.message });
|
|
}
|
|
logger.info('server_stopped_cleanly');
|
|
process.exit(0);
|
|
});
|
|
|
|
// Warn active sockets and terminate connections
|
|
hub.shutdownGracefully();
|
|
|
|
// Force close after 10s maximum timeout
|
|
setTimeout(() => {
|
|
logger.warn('server_shutdown_timeout_force_exit');
|
|
process.exit(1);
|
|
}, 10000).unref();
|
|
}
|
|
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|