webssh2-server
Version:
A Websocket to SSH2 gateway using xterm.js, socket.io, ssh2
198 lines (197 loc) • 8.89 kB
JavaScript
// 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;
}