UNPKG

webssh2-server

Version:

A Websocket to SSH2 gateway using xterm.js, socket.io, ssh2

198 lines (197 loc) 8.89 kB
// server // app/routes.ts // Orchestration layer for HTTP route handling import express, {} from 'express'; import handleConnection from './connectionHandler.js'; import { createNamespacedDebug } from './logger.js'; import { createAuthMiddleware } from './middleware.js'; import { ConfigError, handleError } from './errors.js'; import { HTTP } from './constants.js'; import { processAuthParameters, setupSshCredentials, validateConnectionParams, } from './auth/auth-utils.js'; import { validateSshCredentials } from './connection/index.js'; // Import pure functions from decomposed modules import { createSshValidationErrorResponse, createRouteErrorMessage } from './routes/route-error-handler.js'; import { extractHost, extractPort, extractTerm, validatePostCredentials, validateSessionCredentials, createSanitizedCredentials } from './routes/route-validators.js'; import { createSessionCredentials, createPostAuthSession, getReauthClearKeys, getAuthRelatedKeys } from './routes/session-handler.js'; const debug = createNamespacedDebug('routes'); /** * Handle route errors with proper HTTP responses * Side effect: sends HTTP response */ function handleRouteError(err, res) { const error = new ConfigError(createRouteErrorMessage(err)); handleError(error, res); } /** * Apply error response to HTTP response object * Side effect: sends HTTP response */ function sendErrorResponse(res, errorResponse) { // Set headers if provided if (errorResponse.headers != null) { Object.entries(errorResponse.headers).forEach(([key, value]) => { res.setHeader(key, value); }); } res.status(errorResponse.status).send(errorResponse.message); } /** * Common SSH route handler logic * Handles validation, authentication, and connection setup */ async function handleSshRoute(req, res, connectionParams, config) { // Validate session has credentials if (!validateSessionCredentials(req.session.sshCredentials)) { debug('Missing SSH credentials in session'); res.setHeader(HTTP.AUTHENTICATE, HTTP.REALM); res.status(HTTP.UNAUTHORIZED).send('Missing SSH credentials'); return; } const { username, password } = req.session.sshCredentials; // Validate SSH credentials immediately const validationResult = await validateSshCredentials(connectionParams.host, connectionParams.port, username, password, config); if (validationResult.success === false) { debug(`SSH validation failed for ${username}@${connectionParams.host}:${connectionParams.port}: ${validationResult.errorType} - ${validationResult.errorMessage}`); // Get error response data using pure function const errorResponse = createSshValidationErrorResponse(validationResult, connectionParams.host, connectionParams.port); sendErrorResponse(res, errorResponse); return; } // SSH validation succeeded - proceed with normal flow processAuthParameters(req.query, req.session); const sanitizedCredentials = setupSshCredentials(req.session, { ...connectionParams, username, password }); debug('SSH validation passed - serving client: ', sanitizedCredentials); handleConnection(req, res, { host: connectionParams.host }).catch((error) => { debug('Error handling connection:', error); handleRouteError(error, res); }); } export function createRoutes(config) { const router = express.Router(); const auth = createAuthMiddleware(config); // Root route - uses default config router.get('/', (req, res) => { const r = req; debug('router.get./: Accessed / route'); processAuthParameters(r.query, r.session); handleConnection(req, res).catch((error) => { debug('Error handling connection:', error); handleRouteError(error, res); }); }); // Host route without parameter - uses config default router.get('/host/', auth, async (req, res) => { const r = req; debug(`router.get.host: /ssh/host/ route`); processAuthParameters(r.query, r.session); try { const portParam = r.query['port']; const portNumber = portParam != null && portParam !== '' ? parseInt(portParam, 10) : undefined; const { host, port, term } = validateConnectionParams({ host: config.ssh.host ?? undefined, port: portNumber, sshterm: r.query['sshterm'], config, }); await handleSshRoute(r, res, { host, port, term }, config); } catch (err) { handleRouteError(err, res); } }); // Host route with parameter router.get('/host/:host', auth, async (req, res) => { const r = req; debug(`router.get.host: /ssh/host/${String((req).params['host'])} route`); try { const portParam = r.query['port']; const portNumber = portParam != null && portParam !== '' ? parseInt(portParam, 10) : undefined; const { host, port, term } = validateConnectionParams({ hostParam: r.params['host'], port: portNumber, sshterm: r.query['sshterm'], config, }); await handleSshRoute(r, res, { host, port, term }, config); } catch (err) { handleRouteError(err, res); } }); // Clean POST authentication route for SSO/API integration router.post('/', (req, res) => { const r = req; debug('router.post./: POST /ssh route for SSO authentication'); try { const body = req.body; const query = r.query; // Validate credentials using pure function const credentialValidation = validatePostCredentials(body); if (!credentialValidation.valid) { res.status(400).send('Missing required fields in body: username, password'); return; } // Extract host using pure function const host = extractHost(body, query, config); if (host == null) { res.status(400).send('Missing required field: host (in body or query params)'); return; } // Extract port and term using pure functions const port = extractPort(body, query); const term = extractTerm(body, query); // Create session credentials using pure function const sessionCredentials = createSessionCredentials(host, port, credentialValidation.username ?? '', credentialValidation.password ?? '', term); // Apply to session using pure function pattern const sessionUpdates = createPostAuthSession(sessionCredentials); Object.assign(r.session, sessionUpdates); // Create sanitized log data using pure function const sanitized = createSanitizedCredentials(host, port, credentialValidation.username ?? ''); debug('POST /ssh - Credentials stored in session:', sanitized); debug('POST /ssh - Source: body=%o, query=%o', { host: body['host'], port: body['port'], sshterm: body['sshterm'] }, { host: query['host'] ?? query['hostname'], port: query['port'], sshterm: query['sshterm'] }); // Serve the client page handleConnection(r, res, { host }).catch((error) => { debug('Error handling connection:', error); handleRouteError(error, res); }); } catch (err) { handleRouteError(err, res); } }); // Clear credentials route router.get('/clear-credentials', (req, res) => { const r = req; delete r.session['sshCredentials']; res.status(HTTP.OK).send(HTTP.CREDENTIALS_CLEARED); }); // Force reconnect route router.get('/force-reconnect', (req, res) => { const r = req; delete r.session['sshCredentials']; res.status(HTTP.UNAUTHORIZED).send(HTTP.AUTH_REQUIRED); }); // Re-authentication route router.get('/reauth', (req, res) => { const r = req; debug('router.get.reauth: Clearing session credentials and forcing re-authentication'); // Clear standard auth keys using pure function const clearKeys = getReauthClearKeys(); const session = r.session; clearKeys.forEach(key => { Reflect.deleteProperty(session, key); }); // Clear additional auth-related keys using pure function const authRelatedKeys = getAuthRelatedKeys(Object.keys(session)); authRelatedKeys.forEach(key => { Reflect.deleteProperty(session, key); }); // Redirect to the main SSH page for fresh authentication res.redirect('/ssh'); }); return router; }