/* eslint-disable class-methods-use-this */
const { System } = require('./system');
const utils = require('./utils');
const Console = require('./console');
const Config = require('./config');
const timer = new (require('./timer'))();
/**
* @memberof module:@proceed/system
* @extends module:@proceed/system.System
* @class
* @hideconstructor
*/
class HTTP extends System {
constructor(env) {
super(env);
this.environment = env;
}
_getLogger() {
if (!this.logger) {
this.logger = Console._getLoggingModule().getLogger({ moduleName: 'SYSTEM' });
}
return this.logger;
}
/**
* Set the port on the native part for the communication module.
* @param {number} port The port to use
*/
setPort(port) {
const taskID = utils.generateUniqueTaskID();
const listenPromise = new Promise((resolve, reject) => {
// Listen for the response
this.commandResponse(taskID, (err, data) => {
// Resolve or reject the promise
if (err) {
reject(err);
} else {
resolve(data);
}
return true;
});
});
// Emit the task
this.commandRequest(taskID, ['setPort', [port]]);
return listenPromise;
}
unsetPort() {
const taskID = utils.generateUniqueTaskID();
const listenPromise = new Promise((resolve, reject) => {
// Listen for the response
this.commandResponse(taskID, (err, data) => {
// Resolve or reject the promise
if (err) {
reject(err);
} else {
resolve(data);
}
return true;
});
});
// Emit the task
this.commandRequest(taskID, ['unsetPort', []]);
return listenPromise;
}
/**
* Send a task to open the server for the given path.
* @private
* @param {string} method The HTTP method to listen on
* @param {string} path The path (relative to root)
* @param {object|null} options The options for the server
* @param {boolean} options.cors Whether or not CORS should be enabled
* @param {Function} callback The async callback function
*/
async _serve(method, path, options, callback) {
// options are optional
let [opt, cb] = [options, callback];
if (typeof options === 'function') {
cb = options;
opt = {};
}
// Prepare callback
const taskID = utils.generateUniqueTaskID();
this.commandResponse(taskID, async (_, resID, req) => {
// Process the request with the given callback
let statusCode = 200;
let bodyPeek;
if (req.body) {
const bodyContent = JSON.stringify(req.body);
bodyPeek = bodyContent.length < 20 ? bodyContent : `${bodyContent.substr(0, 20)}...`;
}
// log information about the received request
const bodyInfo = bodyPeek ? `body: ${bodyPeek}` : 'no body';
const params = JSON.stringify(req.params);
const query = JSON.stringify(req.query);
const senderIp = req.ip.substr(req.ip.lastIndexOf(':') + 1);
this._getLogger().trace(
`Received ${req.method} on '${req.path}' from ${senderIp} with params: ${params}, query: ${query} and ${bodyInfo}`
);
this._getLogger().trace(`Received ${req.method} request: ${JSON.stringify(req)}`);
/**
* Callback
*/
const ret = await cb(req).catch((err) => {
this._getLogger().error(`HTTP 404: ${err.message}`);
return {
response: err.message,
mimeType: 'html',
statusCode: err.statusCode || 404,
};
});
// `ret` is either an object or just the sendResponse string
let sendResponse = ret;
let mimeType;
if (typeof ret === 'object') {
sendResponse = ret.response;
({ mimeType } = ret);
statusCode = ret.statusCode || 200;
}
// Send the response back
const resTaskID = utils.generateUniqueTaskID();
this.commandRequest(resTaskID, ['respond', [sendResponse, resID, statusCode, mimeType]]);
});
// Emit the task
this.commandRequest(taskID, ['serve', [method, path, opt]]);
}
/**
* Send a task to open the server for GET requests on the given path.
* @param {string} path The path (relative to root)
* @param {object|null} options The options for the server
* @param {Function} callback The async callback function
*/
get(path, options, callback) {
this._serve('get', path, options, callback);
}
/**
* Send a task to open the server for PUT requests on the given path.
* @param {string} path The path (relative to root)
* @param {object|null} options The options for the server
* @param {Function} callback The async callback function
*/
put(path, options, callback) {
this._serve('put', path, options, callback);
}
/**
* Send a task to open the server for POST requests on the given path.
* @param {string} path The path (relative to root)
* @param {object|null} options The options for the server
* @param {Function} callback The async callback function
*/
post(path, options, callback) {
this._serve('post', path, options, callback);
}
/**
* Send a task to open the server for DELETE requests on the given path.
* @param {string} path The path (relative to root)
* @param {object|null} options The options for the server
* @param {Function} callback The async callback function
*/
delete(path, options, callback) {
this._serve('delete', path, options, callback);
}
/**
* Easy NodeJS http request package on all platforms.
* This method is not using the dispatcher to send the
* task to the underlying system but instead is using the
* request means that are available on this platform.
* @param {string} url The URL for the http request call
* @param {object|null} options The options for the request
*/
async request(url, options, callback, preferNode) {
if (typeof options === 'boolean') {
/* eslint-disable no-param-reassign */
preferNode = options;
options = {};
} else if (!options) {
options = {};
} else if (typeof options === 'function') {
callback = options;
preferNode = callback;
options = {};
} else if (typeof callback === 'boolean') {
preferNode = callback;
callback = undefined;
}
const timeout = await Config._getConfigModule().readConfig('engine.networkRequestTimeout');
const result = new Promise((resolve, reject) => {
timer.setTimeout(() => {
reject('Request timed out (set by `engine.networkRequestTimeout`).');
}, timeout * 1000);
if (options.body !== undefined) {
if (typeof options.body === 'object') {
options.body = JSON.stringify(options.body);
options.headers = {
...options.headers,
'Content-Type': 'application/json',
};
}
options.headers = {
...options.headers,
'Content-Length': this.calcBodyLength(options.body),
};
}
// Make the request
let request;
// In electron, we have both node's require and XMLHttpRequest, but only the
// latter shows up in the Network panel in the dev tools so we prefer that.
// except for request that might try to connect to unreachable engines to avoid errors in V8
if (this.environment === 'web' || (!preferNode && typeof XMLHttpRequest !== 'undefined')) {
request = require('browser-request');
options.uri = url;
if (options.headers) {
delete options.headers['Content-Length'];
}
try {
// 'options' contains everything: method, body, etc.
request(options, (err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
reject(err || { response, body });
} else {
resolve({ response, body });
}
});
} catch (err) {
this._getLogger.info(`Error sending request: ${err}`);
reject(err);
}
} else if (this.environment === 'node') {
// Use node's native require if built with webpack
// eslint-disable-next-line no-undef
const _req = typeof __webpack_require__ === 'function' ? __non_webpack_require__ : require;
if (url.startsWith('https')) {
({ request } = _req('https'));
} else {
({ request } = _req('http'));
}
const urlP = _req('url');
options = { ...options, ...urlP.parse(url) };
const req = request(options, (response) => {
response.setEncoding('utf8');
let body = '';
response.on('data', (chunk) => {
body += chunk;
});
response.on('end', () => {
if (response.statusCode < 200 || response.statusCode >= 300) {
reject({ response, body });
} else {
resolve({ response, body });
}
});
});
req.on('error', (err) => reject(err));
if (options && options.body) {
req.write(options.body);
}
req.end();
}
});
if (callback) {
result
.then((resultObj) => callback(null, resultObj.response, resultObj.body))
.catch((err) => callback(err, null, null));
} else {
return result;
}
}
/**
* Calculates the length of the request body
*
* @param {string} data
* @returns {number} body length
*/
calcBodyLength(data) {
let length;
// Buffer in NodeJS or TextEncoder in browser
if (typeof Buffer !== 'undefined') {
// eslint-disable-next-line no-undef
length = Buffer.byteLength(data);
} else if (typeof TextEncoder !== 'undefined') {
// eslint-disable-next-line no-undef
({ length } = new TextEncoder('utf-8').encode(data));
}
return length;
}
}
module.exports = HTTP;