integreat-transporter-http
Version:
HTTP transporter for Integreat
226 lines • 8.34 kB
JavaScript
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