pip-services3-rpc-node
Version:
Remote procedure calls for Pip.Services in Node.js
429 lines • 19 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpEndpoint = void 0;
/** @module services */
/** @hidden */
const _ = require('lodash');
const fs = require('fs');
const pip_services3_commons_node_1 = require("pip-services3-commons-node");
const pip_services3_components_node_1 = require("pip-services3-components-node");
const pip_services3_components_node_2 = require("pip-services3-components-node");
const pip_services3_commons_node_2 = require("pip-services3-commons-node");
const HttpResponseSender_1 = require("./HttpResponseSender");
const HttpConnectionResolver_1 = require("../connect/HttpConnectionResolver");
/**
* Used for creating HTTP endpoints. An endpoint is a URL, at which a given service can be accessed by a client.
*
* ### Configuration parameters ###
*
* Parameters to pass to the [[configure]] method for component configuration:
*
* - cors_headers - a comma-separated list of allowed CORS headers
* - cors_origins - a comma-separated list of allowed CORS origins
* - connection(s) - the connection resolver's connections:
* - "connection.discovery_key" - the key to use for connection resolving in a discovery service;
* - "connection.protocol" - the connection's protocol;
* - "connection.host" - the target host;
* - "connection.port" - the target port;
* - "connection.uri" - the target URI.
* - credential - the HTTPS credentials:
* - "credential.ssl_key_file" - the SSL private key in PEM
* - "credential.ssl_crt_file" - the SSL certificate in PEM
* - "credential.ssl_ca_file" - the certificate authorities (root cerfiticates) in PEM
*
* ### References ###
*
* A logger, counters, and a connection resolver can be referenced by passing the
* following references to the object's [[setReferences]] method:
*
* - logger: <code>"\*:logger:\*:\*:1.0"</code>;
* - counters: <code>"\*:counters:\*:\*:1.0"</code>;
* - discovery: <code>"\*:discovery:\*:\*:1.0"</code> (for the connection resolver).
*
* ### Examples ###
*
* public MyMethod(_config: ConfigParams, _references: IReferences) {
* let endpoint = new HttpEndpoint();
* if (this._config)
* endpoint.configure(this._config);
* if (this._references)
* endpoint.setReferences(this._references);
* ...
*
* this._endpoint.open(correlationId, (err) => {
* this._opened = err == null;
* callback(err);
* });
* ...
* }
*/
class HttpEndpoint {
constructor() {
this._connectionResolver = new HttpConnectionResolver_1.HttpConnectionResolver();
this._logger = new pip_services3_components_node_1.CompositeLogger();
this._counters = new pip_services3_components_node_2.CompositeCounters();
this._maintenanceEnabled = false;
this._fileMaxSize = 200 * 1024 * 1024;
this._protocolUpgradeEnabled = false;
this._registrations = [];
this._allowedHeaders = ["correlation_id"];
this._allowedOrigins = [];
}
/**
* Configures this HttpEndpoint using the given configuration parameters.
*
* __Configuration parameters:__
* - cors_headers - a comma-separated list of allowed CORS headers
* - cors_origins - a comma-separated list of allowed CORS origins
* - __connection(s)__ - the connection resolver's connections;
* - "connection.discovery_key" - the key to use for connection resolving in a discovery service;
* - "connection.protocol" - the connection's protocol;
* - "connection.host" - the target host;
* - "connection.port" - the target port;
* - "connection.uri" - the target URI.
* - "credential.ssl_key_file" - SSL private key in PEM
* - "credential.ssl_crt_file" - SSL certificate in PEM
* - "credential.ssl_ca_file" - Certificate authority (root certificate) in PEM
*
* @param config configuration parameters, containing a "connection(s)" section.
*
* @see [[https://pip-services3-node.github.io/pip-services3-commons-node/classes/config.configparams.html ConfigParams]] (in the PipServices "Commons" package)
*/
configure(config) {
config = config.setDefaults(HttpEndpoint._defaultConfig);
this._connectionResolver.configure(config);
this._maintenanceEnabled = config.getAsBooleanWithDefault('options.maintenance_enabled', this._maintenanceEnabled);
this._fileMaxSize = config.getAsLongWithDefault('options.file_max_size', this._fileMaxSize);
this._protocolUpgradeEnabled = config.getAsBooleanWithDefault('options.protocol_upgrade_enabled', this._protocolUpgradeEnabled);
let headers = config.getAsStringWithDefault("cors_headers", "").split(",");
for (let header of headers) {
header = header.trim();
if (header != "") {
this._allowedHeaders = this._allowedHeaders.filter(h => h != header);
this._allowedHeaders.push(header);
}
}
let origins = config.getAsStringWithDefault("cors_origins", "").split(",");
for (let origin of origins) {
origin = origin.trim();
if (origin != "") {
this._allowedOrigins = this._allowedOrigins.filter(h => h != origin);
this._allowedOrigins.push(origin);
}
}
}
/**
* Sets references to this endpoint's logger, counters, and connection resolver.
*
* __References:__
* - logger: <code>"\*:logger:\*:\*:1.0"</code>
* - counters: <code>"\*:counters:\*:\*:1.0"</code>
* - discovery: <code>"\*:discovery:\*:\*:1.0"</code> (for the connection resolver)
*
* @param references an IReferences object, containing references to a logger, counters,
* and a connection resolver.
*
* @see [[https://pip-services3-node.github.io/pip-services3-commons-node/interfaces/refer.ireferences.html IReferences]] (in the PipServices "Commons" package)
*/
setReferences(references) {
this._logger.setReferences(references);
this._counters.setReferences(references);
this._connectionResolver.setReferences(references);
}
/**
* Gets an HTTP server instance.
* @returns an HTTP server instance of <code>null</code> if endpoint is closed.
*/
getServer() {
return this._server;
}
/**
* @returns whether or not this endpoint is open with an actively listening REST server.
*/
isOpen() {
return this._server != null;
}
//TODO: check for correct understanding.
/**
* Opens a connection using the parameters resolved by the referenced connection
* resolver and creates a REST server (service) using the set options and parameters.
*
* @param correlationId (optional) transaction id to trace execution through call chain.
* @param callback (optional) the function to call once the opening process is complete.
* Will be called with an error if one is raised.
*/
open(correlationId, callback) {
if (this.isOpen()) {
callback(null);
return;
}
this._connectionResolver.resolve(correlationId, (err, connection, credential) => {
if (err != null) {
callback(err);
return;
}
this._uri = connection.getUri();
try {
let options = {};
if (connection.getProtocolWithDefault('http') == 'https') {
let sslKeyFile = credential.getAsNullableString('ssl_key_file');
let privateKey = fs.readFileSync(sslKeyFile).toString();
let sslCrtFile = credential.getAsNullableString('ssl_crt_file');
let certificate = fs.readFileSync(sslCrtFile).toString();
let ca = [];
let sslCaFile = credential.getAsNullableString('ssl_ca_file');
if (sslCaFile != null) {
let caText = fs.readFileSync(sslCaFile).toString();
while (caText != null && caText.trim().length > 0) {
let crtIndex = caText.lastIndexOf('-----BEGIN CERTIFICATE-----');
if (crtIndex > -1) {
ca.push(caText.substring(crtIndex));
caText = caText.substring(0, crtIndex);
}
}
}
options.key = privateKey;
options.certificate = certificate;
//options.ca = ca;
}
options.handleUpgrades = this._protocolUpgradeEnabled;
// Create instance of restify application
let restify = require('restify');
this._server = restify.createServer(options);
// Configure restify application
this._server.use(restify.plugins.acceptParser(this._server.acceptable));
//this._server.use(restify.authorizationParser());
//this._server.use(restify.CORS());
this._server.use(restify.plugins.dateParser());
this._server.use(restify.plugins.queryParser());
this._server.use(restify.plugins.jsonp());
this._server.use(restify.plugins.gzipResponse());
this._server.use(restify.plugins.jsonBodyParser());
// this._server.use(restify.plugins.bodyParser({
// maxFileSize: this._fileMaxSize
// }));
this._server.use(restify.plugins.conditionalRequest());
//this._server.use(restify.plugins.requestExpiry());
//if (options.get("throttle") != null)
// this._server.use(restify.plugins.throttle(options.get("throttle")));
// Configure CORS requests
let corsMiddleware = require('restify-cors-middleware2');
let origins = this._allowedOrigins;
if (origins.length == 0) {
origins = ["*"];
}
let cors = corsMiddleware({
preflightMaxAge: 5,
origins: origins,
allowHeaders: this._allowedHeaders,
exposeHeaders: this._allowedHeaders
});
this._server.pre(cors.preflight);
this._server.use(cors.actual);
this._server.use((req, res, next) => { this.addCompatibility(req, res, next); });
this._server.use((req, res, next) => { this.noCache(req, res, next); });
this._server.use((req, res, next) => { this.doMaintenance(req, res, next); });
this.performRegistrations();
this._server.listen(connection.getPort(), connection.getHost(), (err) => {
if (err == null) {
// Register the service URI
this._connectionResolver.register(correlationId, (err) => {
this._logger.debug(correlationId, "Opened REST service at %s", this._uri);
if (callback)
callback(err);
});
}
else {
// Todo: Hack!!!
console.error(err);
err = new pip_services3_commons_node_2.ConnectionException(correlationId, "CANNOT_CONNECT", "Opening REST service failed")
.wrap(err).withDetails("url", this._uri);
if (callback)
callback(err);
}
});
}
catch (ex) {
this._server = null;
let err = new pip_services3_commons_node_2.ConnectionException(correlationId, "CANNOT_CONNECT", "Opening REST service failed")
.wrap(ex).withDetails("url", this._uri);
if (callback)
callback(err);
}
});
}
addCompatibility(req, res, next) {
req.param = (name) => {
if (req.query) {
let param = req.query[name];
if (param)
return param;
}
if (req.body) {
let param = req.body[name];
if (param)
return param;
}
if (req.params) {
let param = req.params[name];
if (param)
return param;
}
return null;
};
req.route.params = req.params;
next();
}
// Prevents IE from caching REST requests
noCache(req, res, next) {
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
res.header('Pragma', 'no-cache');
res.header('Expires', 0);
next();
}
// Returns maintenance error code
doMaintenance(req, res, next) {
// Make this more sophisticated
if (this._maintenanceEnabled) {
res.header('Retry-After', 3600);
res.json(503);
}
else
next();
}
/**
* Closes this endpoint and the REST server (service) that was opened earlier.
*
* @param correlationId (optional) transaction id to trace execution through call chain.
* @param callback (optional) the function to call once the closing process is complete.
* Will be called with an error if one is raised.
*/
close(correlationId, callback) {
if (this._server != null) {
// Eat exceptions
try {
this._server.close();
this._logger.debug(correlationId, "Closed REST service at %s", this._uri);
}
catch (ex) {
this._logger.warn(correlationId, "Failed while closing REST service: %s", ex);
}
this._server = null;
this._uri = null;
}
callback(null);
}
/**
* Registers a registerable object for dynamic endpoint discovery.
*
* @param registration the registration to add.
*
* @see [[IRegisterable]]
*/
register(registration) {
this._registrations.push(registration);
}
/**
* Unregisters a registerable object, so that it is no longer used in dynamic
* endpoint discovery.
*
* @param registration the registration to remove.
*
* @see [[IRegisterable]]
*/
unregister(registration) {
this._registrations = _.remove(this._registrations, r => r == registration);
}
performRegistrations() {
for (let registration of this._registrations) {
registration.register();
}
}
fixRoute(route) {
if (route && route.length > 0 && !route.startsWith("/"))
route = "/" + route;
return route;
}
/**
* Returns correlationId from request
* @param req - http request
* @return Returns correlationId from request
*/
getCorrelationId(req) {
let correlationId = req.query.correlation_id;
if (_.isEmpty(correlationId)) {
correlationId = req.headers['correlation_id'];
}
return correlationId;
}
/**
* Registers an action in this objects REST server (service) by the given method and route.
*
* @param method the HTTP method of the route.
* @param route the route to register in this object's REST server (service).
* @param schema the schema to use for parameter validation.
* @param action the action to perform at the given route.
*/
registerRoute(method, route, schema, action) {
method = method.toLowerCase();
if (method == 'delete')
method = 'del';
route = this.fixRoute(route);
// Hack!!! Wrapping action to preserve prototyping context
let actionCurl = (req, res) => {
// Perform validation
if (schema != null) {
let params = _.extend({}, req.params, req.query, { body: req.body });
let correlationId = this.getCorrelationId(req);
let err = schema.validateAndReturnException(correlationId, params, false);
if (err != null) {
HttpResponseSender_1.HttpResponseSender.sendError(req, res, err);
return;
}
}
// Todo: perform verification?
action(req, res);
};
// Wrapping to preserve "this"
let self = this;
this._server[method](route, actionCurl);
}
/**
* Registers an action with authorization in this objects REST server (service)
* by the given method and route.
*
* @param method the HTTP method of the route.
* @param route the route to register in this object's REST server (service).
* @param schema the schema to use for parameter validation.
* @param authorize the authorization interceptor
* @param action the action to perform at the given route.
*/
registerRouteWithAuth(method, route, schema, authorize, action) {
if (authorize) {
let nextAction = action;
action = (req, res) => {
authorize(req, res, () => { nextAction(req, res); });
};
}
this.registerRoute(method, route, schema, action);
}
/**
* Registers a middleware action for the given route.
*
* @param route the route to register in this object's REST server (service).
* @param action the middleware action to perform at the given route.
*/
registerInterceptor(route, action) {
route = this.fixRoute(route);
this._server.use((req, res, next) => {
let match = (req.url.match(route) || []).length > 0;
if (route != null && route != "" && !match)
next();
else
action(req, res, next);
});
}
}
exports.HttpEndpoint = HttpEndpoint;
HttpEndpoint._defaultConfig = pip_services3_commons_node_1.ConfigParams.fromTuples("connection.protocol", "http", "connection.host", "0.0.0.0", "connection.port", 3000, "credential.ssl_key_file", null, "credential.ssl_crt_file", null, "credential.ssl_ca_file", null, "options.maintenance_enabled", false, "options.request_max_size", 1024 * 1024, "options.file_max_size", 200 * 1024 * 1024, "options.connect_timeout", 60000, "options.debug", true);
//# sourceMappingURL=HttpEndpoint.js.map