moleculer-web-uws
Version:
The `moleculer-web-uws` is a fast API gateway service for Moleculer based on µWebSockets.js server. Use it to publish your services over HTTP and WebSockets.
1,031 lines (853 loc) • 32.2 kB
JavaScript
const uws = require('uWebSockets.js');
const { MoleculerError, MoleculerServerError, MoleculerClientError, ServiceNotFoundError } = require('moleculer').Errors;
const { ServiceUnavailableError, NotFoundError, ForbiddenError, RateLimitExceeded, TopicNotFoundError } = Errors = require('./errors');
const bodyParser = require('./bodyparser/bodyparser');
const multipart = require('./bodyparser/multipart');
const StaticServer = require('./staticserver');
const ConnectionContext = require('./websocket/ConnectionContext');
const pipeStream = require('./pipestream');
const keepAlive = require('./websocket/keepAlive');
const handleDisconnect = require('./websocket/handleDisconnect');
const { isObject, isFunction, isString, isNumber, normalizePath, isReadableStream, removeTrailingSlashes } = require('./utils');
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
function getServiceFullname(svc) {
if (svc.version != null && svc.settings.$noVersionPrefix !== true)
return (typeof (svc.version) === 'number' ? 'v' + svc.version : svc.version) + '.' + svc.name;
return svc.name;
}
/**
* uWS API Gateway service for Moleculer microservices framework.
*
* Based on: https://github.com/moleculerjs/moleculer-web
* @service
*/
module.exports = {
name: 'apigateway',
settings: {
static: false,
ws: false,
port: process.env.PORT || 3000,
cors: true,
rateLimit: false,
logging: true,
// Log each request (default to 'info' level)
logRequest: 'info',
// Log the request ctx.params (default to 'debug' level)
logRequestParams: 'debug',
// Log each response (default to 'info' level)
logResponse: 'info',
// If set to true, it will log the response data (default to disable)
logResponseData: false,
// If set to true, it will log 4xx client errors
log4XXResponses: false,
// Log the route registration related activity
logRouteRegistration: 'info',
},
metadata: {
$category: 'gateway',
$description: 'MoleculerJs API Gateway service',
},
actions: {
rest: {
visibility: 'private',
tracing: {
tags: {
params: [
'req.url',
'req.method'
]
},
spanName: ctx => `${ctx.params.req.method} ${ctx.params.req.url}`
},
timeout: 0,
async handler(ctx) {
const req = ctx.params.req;
const res = ctx.params.res;
const route = req.$route;
const endpoint = this.broker.findNextActionEndpoint(route.action);
if (endpoint instanceof Error) {
if (endpoint instanceof ServiceNotFoundError)
throw new ServiceUnavailableError();
throw endpoint;
}
req.$endpoint = endpoint;
const requestID = req.headers['x-correlation-id'] || req.headers['x-request-id'];
if (requestID)
res.setHeader('X-Request-Id', ctx.requestID);
return await this.routeHandler(ctx, req, res);
}
},
},
methods: {
createServer() {
if (this.server) return;
if (this.settings.https && this.settings.https.key && this.settings.https.cert) {
this.server = uws.SSLApp({
key_file_name: this.settings.https.key,
cert_file_name: this.settings.https.cert,
});
this.isHTTPS = true;
} else {
this.server = uws.App();
this.isHTTPS = false;
}
},
listenServer() {
this.server.listen(this.settings.port, listenSocket => {
if (!listenSocket)
throw new MoleculerServerError(`Port ${this.settings.port} is already in use!`);
this.server.listening = true;
this.server.listenSocket = listenSocket;
this.logger.info(`API Gateway Server listening on PORT ${this.settings.port}`);
});
},
createRoutes() {
const processedServices = new Set();
const services = this.broker.registry.services.list({
skipInternal: true,
withActions: true
});
let service;
const servicesLength = services.length;
for (let i = 0; i < servicesLength; ++i) {
service = services[i];
const serviceName = getServiceFullname(service);
if (processedServices.has(serviceName))
continue;
let servicePath = service.settings.rest && service.settings.rest !== ''
? service.settings.rest
: '/' + serviceName;
// Replace periods incase the service is versioned
servicePath = servicePath.replace(/\./g, '/');
const actions = service.actions;
let action = {}, _action;
for (let i = 0, keys = Object.keys(actions); i < keys.length; i++) {
_action = actions[keys[i]];
if (_action.visibility !== 'private') {
action.name = _action.name;
action.authenticate = _action.authenticate;
action.authorize = _action.authorize;
if (_action.rest) {
action.rest = _action.rest;
this.createRestRoute(action, servicePath);
} else if (_action.ws) {
action.ws = _action.ws;
this.createWSocketRoute(action, servicePath);
}
}
}
processedServices.add(serviceName);
}
try {
this.registerPreflightRoute();
this.register404Route();
if (this.settings.ws) {
this.registerWSocketRoutes();
if (this.settings.logRouteRegistration && this.settings.logRouteRegistration in this.logger) {
const entries = this._ws_routes.entries();
for (let [topic, route] of entries) {
this.logger[this.settings.logRouteRegistration]('Pub/Sub:', topic, '=>', route.action);
}
}
}
this.listenServer();
} catch (err) {
throw err;
}
},
createWSocketRoute(action) {
const ws = action.ws;
if (!isObject(ws))
return this.logger.error('The provided ws definition in', action.name, 'is invalid.');
let topic = ws.topic || action.name.replace(/\./g, '\/');
topic = normalizePath(topic);
if (this._ws_routes.has(topic))
return this.logger.error('The provided ws topic in', action.name, 'has already been defined in', this._ws_routes.get(topic).action);
const route = {
action: action.name,
send: ws.send,
publish: ws.publish,
condition: ws.condition
};
this._ws_routes.set(topic, route);
},
registerWSocketRoutes() {
const wsBehaviour = {};
let { path, compression, idleTimeout, maxBackpressure, maxPayloadLength } = isObject(this.settings.ws) ? this.settings.ws : {};
compression = compression ? compression.toUpperCase() : 'SHARED_COMPRESSOR';
if (uws[compression]) wsBehaviour.compression = uws[compression];
if (maxBackpressure) wsBehaviour.maxBackpressure = maxBackpressure;
if (maxPayloadLength) wsBehaviour.maxPayloadLength = maxPayloadLength;
if (idleTimeout) wsBehaviour.idleTimeout = idleTimeout;
wsBehaviour.upgrade = this.wSocketUpgrade;
wsBehaviour.open = this.wSocketOpen;
wsBehaviour.message = this.wSocketMessage;
wsBehaviour.close = this.wSocketClose;
if (this.server) {
const completepath = this.createFullRoutePath(path || '/*');
this.server = this.server.ws(completepath, wsBehaviour);
if (this.settings.logRouteRegistration && this.settings.logRouteRegistration in this.logger)
this.logger[this.settings.logRouteRegistration]('WebSocket:', completepath);
}
},
async wSocketUpgrade(res, _req, context) {
try {
const protocol = _req.getHeader('sec-websocket-protocol');
const key = _req.getHeader('sec-websocket-key');
const extensions = _req.getHeader('sec-websocket-extensions');
let userData = {};
let clientIp = _req.getHeader('x-forwarded-for');
clientIp = (clientIp) ? clientIp : Buffer.from(res.getRemoteAddressAsText()).toString()// returns ipv6 e.g. '0000:0000:0000:0000:0000:ffff:ac11:0001'
const headers = {};
_req.forEach((key, value) => headers[key] = value);
const req = {
headers,
remoteAddress: clientIp,
}
res.onAborted(() => {
res.done = true
});
if (this.settings.ws.authenticate) {
if (!isFunction(this.authenticate))
return this.logger.error('Define \'authenticate\' method in the API Gateway to enable authentication.');
userData.user = await this.authenticate(req, res);
}
if (this.settings.ws.authorize) {
if (!isFunction(this.authorize))
return this.logger.error('Define \'authorize\' method in the API Gateway to enable authentication.');
userData.authz = await this.authorize(req, res);
}
// ALL async calls must come after the message listener, or we'll skip out on messages (e.g. resub after server restart)
if (isFunction(this.settings.ws.upgrade)) {
// Always return user data to be bound to the socket context or empty object
// Client's address is bound by default as clientAddress
const result = await this.settings.ws.upgrade(res, req, context);
Object.assign(userData, result);
}
if (res.done) return
userData.remoteAddress = req.remoteAddress;
res.upgrade(userData, key, protocol, extensions, context);
} catch (err) {
if (res.done) return
res.writeStatus('401').end();
}
},
async wSocketOpen(socket) {
const connectionContext = (socket.connectionContext = new ConnectionContext(socket));
let object;
if (isFunction(this.settings.ws.open)) {
// Always return user data to be bound to the socket context or empty object
// Client's address is bound by default as clientAddress
// Return false if there is an error
try {
object = await this.settings.ws.open(socket);
} catch (err) {
this.logger.error(err);
}
}
const entries = this._ws_routes.entries();
for (let [topic, route] of entries) {
let truthy = route.condition;
if (isFunction(route.condition))
truthy = route.condition(socket);
if (truthy !== false)
socket.subscribe(topic);
}
if (object === false)
return null
else if (object === null)
return null;
else if (!isObject(object))
object = {};
connectionContext.ready();
// Handle PING/PONG; Heartbeats
if (
this.settings.ws.keepAlive && this.settings.ws.keepAlive.ping) {
return keepAlive(connectionContext, this.settings.ws.keepAlive);
}
},
async wSocketMessage(socket, message, isBinary) {
try {
const { connectionContext } = socket;
message = Buffer.from(message);
if (this.settings.ws.keepAlive) {
let pong = this.settings.ws.keepAlive.pong;
if (!(pong instanceof Uint8Array))
this.settings.ws.keepAlive.pong = pong = new Uint8Array([pong]);
if (Buffer.compare(message, pong) === 0)
return keepAlive(connectionContext, this.settings.ws.keepAlive)
}
let route, topic;
try {
const protocol = JSON.parse(decoder.write(message));
if (isObject(protocol)) {
topic = protocol.topic;
if (topic) {
message = protocol.data;
if (this._ws_routes.has(topic))
route = this._ws_routes.get(topic);
else
return socket.send(new TopicNotFoundError());
}
}
} catch (err) {}
if (isFunction(this.settings.ws.message)) {
if (connectionContext.isReady)
message = await this.settings.ws.message(this.server, socket, message, isBinary, topic) || message;
}
if (route) {
const endpoint = this.broker.findNextActionEndpoint(route.action);
if (endpoint instanceof Error) {
if (endpoint instanceof ServiceNotFoundError)
throw new ServiceUnavailableError();
throw endpoint;
}
let result = await this.broker.call(endpoint, message, {
c_ctx: connectionContext // The ConnectionContext is passed to the meta object
});
if (result === undefined || result === null) return;
if (typeof(result) !== 'string' && !(result instanceof ArrayBuffer) && !ArrayBuffer.isView(result)) {
result = this.encodeJsonResponse(result);
}
if (route.send)
socket.send(result);
else if (route.publish)
this.server.publish(topic, result, true);
}
} catch (err) {
const message = err.data && err.data[0] ? err.data[0].message : '';
socket.send(this.encodeJsonResponse({code: err.code, type: err.type, message }));
}
},
wSocketClose(socket, code) {
if (!socket.connectionContext) return
socket.done = true;
try {
handleDisconnect(socket.connectionContext, { exitCode: code });
} catch (err) {
this.logger.error(err);
}
},
createRestRoute(action, servicePath) {
const route = {};
const rest = action.rest;
let methodBpPath;
if (isString(rest))
methodBpPath = rest.split(' ');
else if (isObject(rest)) {
methodBpPath = rest.path || '';
methodBpPath = methodBpPath.split(' ');
route.passReqRes = rest.passReqRes;
route.cors = rest.cors;
if (rest.authenticate) {
if (!isFunction(this.actions.authenticate))
this.logger.error('Define \'authenticate\' method in the API Gateway to enable authentication.');
route.authenticate = true;
}
if (rest.authorize) {
if (!isFunction(this.actions.authorize))
this.logger.error('Define \'authorize\' method in the API Gateway to enable authorization.');
route.authorize = true;
}
} else
return this.logger.error('The provided rest definition in', action.name, 'is invalid.');
let method, bodyParserType, path;
switch(methodBpPath.length) {
case 3:
method = methodBpPath[0];
bodyParserType = methodBpPath[1];
path = methodBpPath[2];
break;
case 2:
method = methodBpPath[0];
path = methodBpPath[1];
break;
case 1:
method = 'GET',
path = methodBpPath[0];
break;
case 0:
method = '*';
break;
}
if (!path) {
path = action.name.replace(/\./g, '\/');
} else {
// Check for named parameters
if (path.indexOf(':') > -1) {
const namedParams = path.split(':').splice(1);
route.namedParams = namedParams.map((param) => removeTrailingSlashes(param))
}
path = servicePath + '/' + path;
path = normalizePath(path);
}
// Handle paths such as /test/hello/*/* -> /test/hello/*
route.path = path.replace(/(?:\/\*){2,}/g, '/*');
if (method === '*')
method = 'any';
else {
method = method.toLowerCase();
if (method === 'delete')
method = 'del';
}
if (!['get', 'post', 'del', 'put', 'options', 'head'].includes(method))
return this.logger.error('Unsurpported HTTP Method \'', method, '\'!');
route.method = method;
route.action = action.name;
// Resolve the body parser at build time
if (rest.multipart || bodyParserType === 'multipart') {
const opts = Object.assign({}, this.settings.multipart, rest.multipart);
route.multipart = opts;
route.bodyParser = 'multipart';
} else if (bodyParserType)
route.bodyParser = bodyParserType;
else
route.bodyParser = 'any';
this.registerRestRoute(route);
},
registerRestRoute(route) {
if (this.server) {
let resolvedRoute;
try {
resolvedRoute = this.addRestRoute(route);
} catch (err) {
return this.logger.error('RouteError!', err.name, ':', err.message);
}
const completepath = this.createFullRoutePath(resolvedRoute.path);
// Top Level Path
const TLP_LENGTH = this.settings.path
? this.settings.path.length - 1
: 0
this.server = this.server[route.method](completepath, async (res, _req) => {
const $startTime = process.hrtime();
const headers = {};
const params = {};
_req.forEach((key, value) => headers[key] = value);
if (route.namedParams) {
for (let i = 0; i < route.namedParams.length; i++) {
const qp = _req.getParameter(i);
if (qp) params[route.namedParams[i]] = qp;
}
}
const req = {
$startTime,
$route: route,
headers,
method: _req.getMethod().toUpperCase(),
query: _req.getQuery(),
params,
url: _req.getUrl().substring(TLP_LENGTH),
remoteAddress: Buffer.from(res.getRemoteAddressAsText()).toString(),
}
this.logRequest(req);
res = this.patchHttpRes(res, req);
try {
// CORS headers
if (this.settings.cors) {
if (route.cors) Object.assign(this.settings.cors, route.cors);
this.writeCorsHeaders(req, res);
}
await this.actions.rest({ req, res });
} catch (err) {
// Clientside error logging
if (this.settings.log4XXResponses && 400 < err.code && err.code < 500)
this.logger.error('Request error!', err.name, ':', err.message, '\n', err.stack, '\nData:', err.data);
return this.sendError(req, res, err);
}
});
if (this.settings.logRouteRegistration && this.settings.logRouteRegistration in this.logger)
this.logger[this.settings.logRouteRegistration]('HTTP:', route.method.toUpperCase(), completepath, '=>', route.action);
}
},
addRestRoute(route) {
let {path, method} = route;
if (this._routes[method]) {
if (this._routes[method][path])
return this.logger.error('RouteError:', method.toUpperCase(),' ', path, ' already exists');
else
this._routes[method][path] = route;
} else {
this._routes[method] = {
[path]: route,
};
}
return route;
},
createFullRoutePath(path) {
let completepath = this.settings.path
? this.settings.path + '/' + path
: path;
completepath = normalizePath('/' + completepath);
if (completepath.endsWith('/'))
completepath = completepath.slice(0, -1);
return completepath;
},
registerPreflightRoute() {
const completepath = this.createFullRoutePath('/*');
this.server = this.server.options(completepath, (res, _req) => {
const $startTime = process.hrtime();
const req = {
$startTime,
url: _req.getUrl(),
method: _req.getMethod().toUpperCase(),
headers: {
origin: _req.getHeader('origin'),
'access-control-request-method': _req.getHeader('access-control-request-method')
}
}
this.logRequest(req);
res = this.patchHttpRes(res, req);
try {
if (this.settings.cors)
this.writeCorsHeaders(req, res, true);
if (req.headers['access-control-request-method']) {
res.statusCode = 204;// No content
res.setHeader('Content-Length', 0).end();
}
} catch (err) {
res.statusCode = 500;
res.end('Internal Server Error!');
}
});
},
register404Route() {
this.server = this.server.any('/*', (res, _req) => {
const $startTime = process.hrtime();
const req = { $startTime, url: _req.getUrl(), method: _req.getMethod().toUpperCase() }
this.logRequest(req);
res = this.patchHttpRes(res, req);
try {
this.sendError(req, res, new NotFoundError());
if (!res.done) throw new Error();
} catch (err) {
res.statusCode = 500;
res.end('Internal Server Error!');
}
})
},
patchHttpRes(res, req) {
res.onAborted(() => { res.done = true });
res.statusCode = 200;
res._headers = new Map();
res.setHeader = (key, value) => {
res._headers.set(key+'', value+'');
return res;
}
res.getHeader = (key) => {
return res._headers.get(key);
}
res.removeHeader = (key) => {
res._headers.delete(key);
return res;
}
res._end = res.end;
res.end = (body) => {
if (res.done) return;
res.done = true;
res.writeStatus((res.statusCode)+'');
for (let [key, value] of res._headers) {
res.writeHeader(key, value);
}
res._end(body);
// Log the response if configured
this.logResponse(req, res.statusCode, body);
}
return res;
},
async routeHandler(ctx, req, res) {
const route = req.$route;
route.callOptions = route.callOptions ? { ...route.callOptions } : {};
// Rate limiter
if (this.settings.rateLimit && isObject(this.settings.rateLimit)) {
const opts = this.settings.rateLimit;
const store = this.settings.store;
const key = opts.key(req);
if (key) {
const remaining = opts.limit - store.inc(key);
if (opts.headers) {
res.setHeader('X-Rate-Limit-Limit', opts.limit);
res.setHeader('X-Rate-Limit-Remaining', Math.max(0, remaining));
res.setHeader('X-Rate-Limit-Reset', store.resetTime);
}
if (remaining < 0) throw new RateLimitExceeded();
}
}
req.query = this.parseQueryString(req.query);
if (route.authenticate)
ctx.meta.auth = await this.actions.authenticate({ req, res });
if (route.authorize)
ctx.meta.authz = await this.actions.authorize({ req, res });
if (route.multipart)
return await this.multipartHandler(ctx, req, res);
let body = await bodyParser(route.bodyParser, req, res);
let params = req.params;
if (body && !isObject(body)) {
if (Object.keys(params).length === 0 && Object.keys(req.query).length === 0)
params = body;
else
Object.assign(params, req.query, { 0: body });
} else {
Object.assign(params, req.query, body);
}
return await this.callAction(ctx, req, res, params);
},
async callAction(ctx, req, res, params) {
const route = req.$route;
const actionName = route.action;
// onBeforeCall handling
if (this.settings.onBeforeCall)
await this.settings.onBeforeCall.call(this, ctx, req, res);
// Logging params
if (this.settings.logging) {
if (this.settings.logRequest && this.settings.logRequest in this.logger)
this.logger[this.settings.logRequest](`Call '${actionName}' action`);
if (this.settings.logRequestParams && this.settings.logRequestParams in this.logger)
this.logger[this.settings.logRequestParams]('Params:', params);
}
Object.assign(ctx.meta, route.callOptions);
// Pass the `req` & `res` vars to ctx.meta.
if (route.passReqRes) {
ctx.meta.$req = req;
ctx.meta.$res = res;
}
// Transfer URL parameters via meta in case of stream
if (params && params.$params)
ctx.meta.$params = params.$params;
let data = await ctx.call(req.$endpoint, params);
if (data instanceof Error)
return this.sendError(req, res, data);
// onAfterCall handling
if (this.settings.onAfterCall)
data = await this.settings.onAfterCall.call(this, ctx, req, res, data);
this.sendResponse(ctx, req, res, data);
return true;
},
writeCorsHeaders(req, res, isPreFlight) {
const origin = req.headers['origin'];
const cors = this.settings.cors;
// Access-control-allow-origin
if (!cors.origin || cors.origin === '*' || (Array.isArray(cors.origin) && cors.origin.includes('*')))
res.setHeader('Access-Control-Allow-Origin', '*');
else if (origin && this.checkOrigin(origin, cors.origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
} else
throw new ForbiddenError('ORIGIN_NOT_ALLOWED');
// Access-Control-Allow-Credentials
if (cors.credentials === true)
res.setHeader('Access-Control-Allow-Credentials', 'true');
// Access-Control-Expose-Headers
if (isString(cors.exposedHeaders))
res.setHeader('Access-Control-Expose-Headers', cors.exposedHeaders);
else if (Array.isArray(cors.exposedHeaders))
res.setHeader('Access-Control-Expose-Headers', cors.exposedHeaders.join(', '));
if (isPreFlight) {
// Access-Control-Allow-Headers
if (isString(cors.allowedHeaders))
res.setHeader('Access-Control-Allow-Headers', cors.allowedHeaders);
else if (Array.isArray(cors.allowedHeaders))
res.setHeader('Access-Control-Allow-Headers', cors.allowedHeaders.join(', '));
else {
// AllowedHeaders not specified, send back from req headers
const allowedHeaders = req.headers['access-control-request-headers'];
if (allowedHeaders) {
res.setHeader('Vary', 'Access-Control-Request-Headers');
res.setHeader('Access-Control-Allow-Headers', allowedHeaders);
}
}
// Access-Control-Allow-Methods
if (isString(cors.methods))
res.setHeader('Access-Control-Allow-Methods', cors.methods);
else if (Array.isArray(cors.methods))
res.setHeader('Access-Control-Allow-Methods', cors.methods.join(', '));
// Access-Control-Max-Age
if (cors.maxAge)
res.setHeader('Access-Control-Max-Age', cors.maxAge);
}
},
async multipartHandler(ctx, req, res) {
const route = req.$route;
const multipartOptions = route.multipart;
const result = await multipart(req, res, multipartOptions);
const { fields, files } = result;
const numOfFiles = files.length;
// Add limit event handlers
if (result.fileSizeLimitExceeded && isFunction(multipartOptions.onFileSizeLimit)) {
for (let i = 0; i < numOfFiles; i++) {
multipartOptions.onFileSizeLimit.call(this.service, files[i].size, multipartOptions.fileSize);
}
}
if (result.filesLimitExceeded && isFunction(multipartOptions.onFilesLimit))
multipartOptions.onFilesLimit.call(this.service, numOfFiles, multipartOptions.files);
if (result.fieldsLimitExceeded && isFunction(multipartOptions.onFieldsLimit))
multipartOptions.onFieldsLimit.call(this.service, fields.length, multipartOptions.fields);
if (multipartOptions.empty === false && numOfFiles === 0)
throw new MoleculerClientError('File missing in the request!');
ctx.meta.$multipart = fields;
return await this.callAction(ctx, req, res, files.length > 1 ? files : files[0]);
},
encodeJsonResponse(data) {
return JSON.stringify(data);
},
sendResponse(ctx, req, res, data) {
// Custom status code from ctx.meta
if (ctx.meta.$statusCode)
res.statusCode = ctx.meta.$statusCode;
if (ctx.meta.$responseHeaders) {
for (let i = 0, keys = Object.keys(ctx.meta.$responseHeaders); i < keys.length; i++) {
res.setHeader(keys[i], ctx.meta.$responseHeaders[keys[i]]);
}
}
// Redirect
if (res.statusCode >= 300 && res.statusCode < 400 && res.statusCode !== 304) {
const location = ctx.meta.$location;
if (!location)
this.logger.warn(`The 'ctx.meta.$location' is missing for status code '${res.statusCode}'!`);
else
res.setHeader('Location', location);
}
if (req.method === 'HEAD')
res.cork(() => res.end());
if (data === null)
return res.cork(() => res.end())
let chunk;
if (Buffer.isBuffer(data)) {// Buffer
!res.getHeader('Content-Type') && res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Length', data.length);
chunk = data;
} else if (isReadableStream(data)) {// Stream
!res.getHeader('Content-Type') && res.setHeader('Content-Type', 'application/octet-stream');
chunk = data;
} else if (isObject(data) || Array.isArray(data)) {
!res.getHeader('Content-Type') && res.setHeader('Content-Type', 'application/json; charset=utf-8');
chunk = this.encodeJsonResponse(data);
} else {// Other (stringify or raw text)
!res.getHeader('Content-Type') && res.setHeader('Content-Type', 'text/plain');
if (isString(data)) chunk = data;
else chunk = data.toString();
}
// // Auto generate & add ETag
// if (this.settings.etag && chunk && !isReadableStream(chunk)) {
// res.setHeader('ETag', generateETag.call(this, chunk, route.etag));
// // Freshness
// if (isFresh(req, res))
// res.statusCode = 304;
// }
if (res.statusCode === 204 || res.statusCode === 304) {
res.removeHeader('Content-Type');
res.removeHeader('Content-Length');
res.removeHeader('Transfer-Encoding');
res.cork(() => res.end());
} else {
if (isReadableStream(data)) {
if (typeof(ctx.meta.$byteLength) !== 'number') {
res.statusCode = 500;
return res.cork(() => res.end(chunk));
}
res.writeStatus((res.statusCode)+'');
for (let [key, value] of res._headers) {
res.writeHeader(key, value);
}
pipeStream(res, data, ctx.meta.$byteLength);
this.logResponse(req, res.statusCode);
} else
res.cork(() => res.end(chunk));
}
},
sendError(req, res, err) {
if (isFunction(this.settings.onError))
return this.settings.onError.call(this, req, res, err);
if (!err || !(err instanceof Error)) {
res.statusCode = 500;
return res.end('Internal Server Error!');
}
if (!(err instanceof MoleculerError)) {
const e = err;
err = new MoleculerError(e.message, e.code || e.status, e.type, e.data);
err.name = e.name;
}
const statusCode = isNumber(err.code) && (400 < err.code && err.code < 599) ? err.code : 500;
res.statusCode = statusCode;
const { name, message, code, type, data } = err;
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.end(err ? this.encodeJsonResponse({ name, message, code, type, data }) : 'Internal Server Error!');
},
parseQueryString(qs) {
const params = {};
if (qs) {
const props = new URLSearchParams(qs);
for (const [key, value] of props) {
if (key.endsWith('[]'))
params[key.slice(0, -2)] = props.getAll(key);
else
params[key] = value;
}
}
return params;
},
logRouteRegistration(...args) {
if (this.settings.logging && this.settings.logRouteRegistration && this.settings.logRouteRegistrationin in this.logger)
this.logger[this.settings.logRouteRegistration](...args);
},
logRequest(req) {
if (this.settings.logging && this.settings.logRequest && this.settings.logRequest in this.logger)
this.logger[this.settings.logRequest](`=> ${req.method} ${req.url}`);
},
logResponse(req, statusCode, data) {
if (this.settings.logging && this.settings.logResponse && this.settings.logResponse in this.logger) {
const diff = process.hrtime(req.$startTime);
const duration = (diff[0] + diff[1] / 1e9) * 1000;
const time = `[+${Number(duration).toFixed(3)} ms]`;
let messageLog = `<= ${statusCode} ${req.method} ${req.url} ${time}`;
if (this.settings.logResponseData)
messageLog += ' Data:', data;
this.logger[this.settings.logResponse](messageLog);
}
},
checkOrigin(origin, settings) {
if (isString(settings)) {
if (settings.indexOf(origin) !== -1)
return true;
if (settings.indexOf('*') !== -1) {
// Based on: https://github.com/hapijs/hapi
const wildcard = new RegExp(`^${settings.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&').replace(/\\\*/g, '.*').replace(/\\\?/g, '.')}$`);
return origin.match(wildcard);
}
} else if (Array.isArray(settings)) {
for (let i = 0, len = settings.length; i < len; i++) {
if (this.checkOrigin(origin, settings[i])) return true;
}
}
return false;
},
},
events: {
'$services.changed'(...args) {
// Clear routes and rebuild them
// this._routes = {};
// this.createRoutes();
},
'$broker.started'() {
this.createRoutes();
}
},
created() {
// Create a new HTTP/HTTPS server instance
this.createServer();
// Create static server service
if (this.settings.assets) {
Object.assign(StaticServer.settings, this.settings.assets)
this.broker.createService(StaticServer);
}
this._routes = {};
this._ws_routes = new Map();
this.logger.info('API Gateway Server created.');
},
stopped() {
if (this.server && this.server.listening)
uws.us_listen_socket_close(this.server.listenSocket);
},
Errors,
StaticServer
}