UNPKG

integreat-transporter-http

Version:

HTTP transporter for Integreat

226 lines 8.34 kB
import debugFn from 'debug'; import { actionFromRequest } from './utils/request.js'; import { dataFromResponse, statusCodeFromResponse, normalizeHeaders, } from './utils/response.js'; const debug = debugFn('integreat:transporter:http'); const debugHeaders = debugFn('integreat:transporter:http:headers'); const matchesHostname = (hostname, patterns) => patterns.length === 0 ? 1 : typeof hostname === 'string' && patterns.includes(hostname) ? 10000 : 0; const matchPattern = (path) => function matchPattern(score, pattern) { const isMatch = path.startsWith(pattern) && (path.length === pattern.length || ['/', '?', '#'].includes(pattern[path.length])); return isMatch && pattern.length > score ? pattern.length : score; }; function matchesPath(path, patterns) { if (patterns.length === 0 || patterns.includes('/')) { return 1; } return typeof path === 'string' ? patterns.reduce(matchPattern(path.toLowerCase()), 0) : 0; } function actionMatchesOptions(action, options) { const hostScore = matchesHostname(action.payload.hostname, options.host); const pathScore = hostScore > 0 ? matchesPath(action.payload.path, options.path) : 0; return pathScore > 0 ? hostScore + pathScore : 0; } const lowerCaseActionPath = (action) => ({ ...action, payload: { ...action.payload, path: typeof action.payload.path === 'string' ? action.payload.path.toLowerCase() : undefined, }, }); const setIdentAndSourceService = (action, ident, sourceService) => typeof sourceService === 'string' ? { ...action, payload: { ...action.payload, sourceService, }, meta: { ...action.meta, ident }, } : { ...action, meta: { ...action.meta, ident } }; function respond(res, statusCode, responseData, responseHeaders) { try { const headers = normalizeHeaders(responseHeaders); res .writeHead(statusCode, { 'content-type': 'application/json', ...headers, }) .end(responseData); } catch { res .writeHead(500) .end(JSON.stringify({ status: 'error', error: 'Internal server error' })); } } function wwwAuthHeadersFromOptions(options) { const { challenges } = options || {}; if (Array.isArray(challenges) && challenges.length > 0) { const challenge = challenges[0]; const params = [ ...(challenge.realm ? [`realm="${challenge.realm}"`] : []), ...Object.entries(challenge.params).map(([key, value]) => `${key}="${value}"`), ].join(', '); return { ['www-authenticate']: `${challenge.scheme}${params ? ` ${params}` : ''}`, }; } return {}; } function getHeadersAndSetAuthHeaders(response, options) { if (response.status === 'noaccess' && response.reason === 'noauth') { return { ...response.headers, ...wwwAuthHeadersFromOptions(options) }; } else { return response.headers; } } const setResponseIfAuthError = (action, response) => response.status !== 'ok' ? { ...action, response } : action; function sortMatches([a], [b]) { return b - a; } function findMatchingHandlerCase(handlerCases, action) { const matched = []; for (const handleCase of handlerCases.values()) { const score = actionMatchesOptions(action, handleCase.options); if (score > 0) { matched.push([score, handleCase]); } } if (matched.length > 0) { return matched.sort(sortMatches)[0][1]; } else { return undefined; } } async function authAndPrepareAction(action, { options, authenticate }) { const authResponse = await authenticate({ status: 'granted' }, action); const ident = authResponse.access?.ident; const sourceService = options?.sourceService; const authenticatedAction = setResponseIfAuthError(setIdentAndSourceService(action, ident, sourceService), authResponse); return options.caseSensitivePath ? authenticatedAction : lowerCaseActionPath(authenticatedAction); } const createHandler = (ourServices, incomingPort) => async function handleIncoming(req, res) { const action = await actionFromRequest(req, incomingPort); debug(`Incoming action: ${action.type} ${action.payload.method} ${action.payload.path} ${action.payload.queryParams} ${action.payload.contentType}`); debugHeaders(`Incoming headers: ${JSON.stringify(req.headers)}`); const handleCase = findMatchingHandlerCase(ourServices, action); if (!handleCase || !handleCase.dispatch || !handleCase.authenticate) { res.writeHead(404); res.end(); return; } const { options, dispatch } = handleCase; const incomingAction = await authAndPrepareAction(action, handleCase); const response = await dispatch(incomingAction); const responseData = dataFromResponse(response); const statusCode = statusCodeFromResponse(response); const headers = getHeadersAndSetAuthHeaders(response, options); respond(res, statusCode, responseData, headers); }; function getErrorFromConnection(connection) { if (!connection) { return { status: 'badrequest', error: 'Cannot listen to server. No connection', }; } else if (!connection.incoming) { return { status: 'noaction', error: 'Service not configured for listening', }; } else if (!connection.server) { return { status: 'badrequest', error: 'Cannot listen to server. No server set on connection', }; } else if (!connection.incoming.port) { return { status: 'badrequest', error: 'Cannot listen to server. No port set on incoming options', }; } else { return { status: 'error', error: 'Cannot listen to server. Unknown error', }; } } function getHandlerCasesForPort(portHandlers, port) { let handlerCases = portHandlers.get(port); if (!handlerCases) { handlerCases = new Set(); portHandlers.set(port, handlerCases); } return handlerCases; } function waitForListeningOrError(server) { return new Promise((resolve, reject) => { const listeningFn = () => { removeListeners(); resolve(undefined); }; const errorFn = (err) => { removeListeners(); reject(err); }; const removeListeners = () => { server.removeListener('listening', listeningFn); server.removeListener('listening', errorFn); }; server.on('listening', listeningFn); server.on('error', errorFn); }); } export default (portHandlers) => async function listen(dispatch, connection, authenticate) { debug('Start listening ...'); const { incoming, server } = connection || {}; if (!connection || !incoming?.port || !server) { const errorResponse = getErrorFromConnection(connection); debug(errorResponse.error); return errorResponse; } const handlerCases = getHandlerCasesForPort(portHandlers, incoming.port); if (server.listening) { debug(`Already listening on port ${incoming.port}`); } else { debug(`Set up request handler for first service on port ${incoming.port}`); const handler = createHandler(handlerCases, incoming.port); server.on('request', handler); try { debug(`Start listening to first service on port ${incoming.port}`); server.listen(incoming.port); await waitForListeningOrError(server); debug(`Listening on port ${incoming.port}`); } catch (error) { debug(`Cannot listen to server on port ${incoming.port}. ${error}`); return { status: 'error', error: `Cannot listen to server on port ${incoming.port}. ${error}`, }; } } const handlerCase = { options: incoming, dispatch, authenticate }; handlerCases.add(handlerCase); connection.handlerCase = handlerCase; return { status: 'ok' }; }; //# sourceMappingURL=listen.js.map