node-gotapi
Version:
The node-gotapi is a Node.js implementation of the Generic Open Terminal API Framework (GotAPI) developed by the Open Mobile Alliance (OMA).
495 lines (460 loc) • 14.3 kB
JavaScript
/* ------------------------------------------------------------------
* node-gotapi - gotapi-interface-1.js
*
* Copyright (c) 2017-2019, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2019-10-20
* ---------------------------------------------------------------- */
'use strict';
let mOs = require('os');
let mCrypto = require('crypto');
let mIPRest = require('./ip-address-restriction.js');
let mWebContentProvider = require('./web-content-provider.js');
/* ------------------------------------------------------------------
* Constructor: GotapiInterface1(config, sendToGotapiServer)
* ---------------------------------------------------------------- */
let GotapiInterface1 = function (config, sendToGotapiServer) {
this.config = config;
this.sendToGotapiServer = sendToGotapiServer;
this.local_address_list = ['localhost'];
this.requests = {};
this.http_request_timeout = 65; // Seconds
this.port = 0;
this.server = null;
this.status_codes = null;
if (this.config['ssl_engine'] === true) {
let https = require("https");
this.server = https.createServer({
key: this.config['ssl_key_data'],
cert: this.config['ssl_crt_data'],
ca: this.config['ssl_ca_data']
});
this.port = this.config['gotapi_if_ssl_port'];
let http = require('http');
this.status_codes = http.STATUS_CODES;
} else {
let http = require('http');
this.server = http.createServer();
this.port = this.config['gotapi_if_port'];
this.status_codes = http.STATUS_CODES;
}
this.oncommunication = null;
this.error_code_map = require('./error-code.json');
// For Web contents
this.web_content_provider = null;
};
/* ------------------------------------------------------------------
* Method: start()
* ---------------------------------------------------------------- */
GotapiInterface1.prototype.start = function () {
let promise = new Promise((resolve, reject) => {
// For Web contents
this.web_content_provider = new mWebContentProvider(this.config);
// Get the local IP addresses
let netifs = mOs.networkInterfaces();
for (let dev in netifs) {
netifs[dev].forEach((info) => {
this.local_address_list.push(info.address);
});
}
// Start the HTTP server for the GotAPI-1 Interface
try {
this.server.on('error', (error) => {
reject(error);
});
this.server.listen(this.port, () => {
this.server.on('request', (req, res) => {
let url_path_parts = (req.url.split('?'))[0].split('/');
if (url_path_parts[1] === 'gotapi') {
this._monitorIncoming(req);
this._receiveRequestFromApp(req, res);
} else {
this.web_content_provider.receiveRequest(req, res);
}
});
this.server.removeAllListeners('error');
resolve();
});
} catch (error) {
reject(error);
}
// Start to watch http connections
this._watchHttpConnections();
});
return promise;
};
GotapiInterface1.prototype._monitorIncoming = function (req) {
if (!this.oncommunication) {
return;
}
this.oncommunication({
type: 1,
dir: 1,
url: req.url,
method: req.method,
headers: req.headers
});
};
GotapiInterface1.prototype._monitorOutgoing = function (m) {
if (!this.oncommunication) {
return;
}
this.oncommunication({
type: 1,
dir: 2,
code: m['code'],
headers: m['headers'],
data: m['data']
});
};
GotapiInterface1.prototype._watchHttpConnections = function () {
let now = Date.now();
Object.keys(this.requests).forEach((request_id) => {
let request = this.requests[request_id];
if (now - request['ctime'] > this.http_request_timeout * 1000) {
this._returnErrorToApp(
request_id,
this.error_code_map['TIMEOUT'],
'TIMEOUT: The GotAPI Server did not respond.'
);
}
});
setTimeout(() => {
this._watchHttpConnections();
}, 1000);
};
GotapiInterface1.prototype._receiveRequestFromApp = function (req, res) {
let origin = this._getOrigin(req);
if (origin && !req.headers['origin']) {
req.headers['origin'] = origin;
}
let ip_allowed = mIPRest.isArrowed(req.connection.remoteAddress, this.config['allowed_address_list']);
if (ip_allowed) {
if (this._checkOrigin(req)) {
let method = req.method.toLowerCase();
if (method === 'options') {
this._retunForPreflightRequestToApp(req, res);
} else if (method.match(/^(get|post|put|delete)$/)) {
this._handleRequestFromApp(req, res);
} else {
this._retun403ToApp(
req, res,
this.error_code_map['INVALID_METHOD'],
'The HTTP method `' + method + '` is not allowed.'
);
}
} else {
this._retun403ToApp(
req, res,
this.error_code_map['INVALID_ORIGIN'],
'The access from the origin `' + req.headers['origin'] + '` is not allowed.'
);
}
} else {
this._retun403ToApp(
req, res,
this.error_code_map['NOT_AUTHORIZED'],
'The access from the IP address `' + req.connection.remoteAddress + '` is not allowed.'
);
}
};
GotapiInterface1.prototype._retun403ToApp = function (req, res, error_code_obj, error_message) {
let headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': req.headers['origin'] || '*'
};
let code = error_code_obj['statusCode'];
res.writeHead(code, headers);
let data = {
result: error_code_obj['result'],
errorCode: error_code_obj['errorCode'],
errorText: this.status_codes[code] || 'Unknown Error',
errorMessage: '[' + error_code_obj['errorName'] + '] ' + error_message,
statusCode: error_code_obj['statusCode']
};
res.write(JSON.stringify(data));
res.end();
this._monitorOutgoing({ code: error_code_obj['statusCode'], data: data, headers: headers });
};
GotapiInterface1.prototype._getOrigin = function (req) {
let origin = req.headers['origin'];
if (origin) {
return origin;
}
let referer = req.headers['referer'];
if (referer) {
let m = referer.match(/^(https*\:\/\/[^\/]+)/);
if (m && m[1]) {
return m[1];
}
}
return '';
};
GotapiInterface1.prototype._checkOrigin = function (req) {
if (this.config['disable_auth']) {
return true;
}
let origin = req.headers['origin'];
if (!origin) {
return false;
}
let scheme = '';
let port1 = 0;
let port2 = 0;
if (this.config['ssl_engine'] === true) {
scheme = 'https:';
port1 = this.config['gotapi_if_ssl_port'];
port2 = this.config['https_server_port'];
} else {
scheme = 'http:';
port1 = this.config['gotapi_if_port'];
port2 = this.config['http_server_port'];
}
let allowed = false;
if (!/\.local/.test(origin)) {
for (let i = 0; i < this.local_address_list.length; i++) {
let addr = this.local_address_list[i];
let list = [
scheme + '//' + addr + ':' + port1,
scheme + '//' + addr + ':' + port2,
scheme + '//[' + addr + ']:' + port1,
scheme + '//[' + addr + ']:' + port2,
];
list.forEach((u) => {
if (origin === u) {
allowed = true;
}
});
}
if (allowed === true) {
return allowed;
}
}
let allowed_origin_list = this.config['allowed_origin_list'];
if (allowed_origin_list && Array.isArray(allowed_origin_list) && allowed_origin_list.length > 0) {
for (let i = 0; i < allowed_origin_list.length; i++) {
if (origin === allowed_origin_list[i]) {
allowed = true;
break;
}
}
}
return allowed;
};
GotapiInterface1.prototype._retunForPreflightRequestToApp = function (req, res) {
let headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': req.headers['origin'] || '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS, PUT, DELETE'
};
res.writeHead(200, headers);
res.write('');
res.end();
this._monitorOutgoing({ code: 200, data: null, headers: headers });
};
GotapiInterface1.prototype._handleRequestFromApp = function (req, res) {
let request_id = this._createNewRequestId();
let url_path_parts = (req.url.split('?'))[0].split('/');
let params_parse_result = this._parseQueryStringFromUrl(req.url);
this.requests[request_id] = {
req: req,
res: res,
ctime: Date.now(),
message: {
if_type: 1,
request_id: request_id,
request_url: req.url,
params: params_parse_result['result'],
package: this.config['disable_auth'] ? 'dummy' : req.headers['origin'],
api: url_path_parts[1],
profile: url_path_parts[2] || '',
attribute: url_path_parts[3] || '',
method: req.method.toLowerCase()
}
};
if (params_parse_result['error']) {
this._returnErrorToApp(
request_id,
this.error_code_map['INVALID_PARAMETER'],
'Failed to parse parameters: ' + params_parse_result['error']
);
return;
}
let ctype = req.headers['content-type'] || '';
if (ctype === 'application/x-www-form-urlencoded') {
let body = '';
req.on('data', (data) => {
body += data;
});
req.on('end', () => {
let p = this._parseQueryString(body);
if (p['error']) {
this._returnErrorToApp(
request_id,
this.error_code_map['INVALID_PARAMETER'],
'Failed to parse parameters: ' + p['error']
);
return;
}
if (p['result']) {
let params = this.requests[request_id]['message']['params'];
for (let k in p['result']) {
params[k] = p['result'][k];
}
}
let message = JSON.parse(JSON.stringify(this.requests[request_id]['message']));
this.sendToGotapiServer(message);
});
} else if (ctype.match(/^multipart\/form\-data/)) {
this._returnErrorToApp(
request_id,
this.error_code_map['INVALID_METHOD'],
'The content type `multipart/form-data` is not supported.'
);
} else {
let message = JSON.parse(JSON.stringify(this.requests[request_id]['message']));
this.sendToGotapiServer(message);
}
};
GotapiInterface1.prototype._createNewRequestId = function () {
let id = '';
id += mCrypto.randomBytes(32).toString('hex') + '_';
id += Date.now();
let sha256 = mCrypto.createHash('sha256');
sha256.update(id);
id = sha256.digest('hex');
return id;
};
GotapiInterface1.prototype._returnErrorToApp = function (request_id, error_code_obj, error_message) {
let request = this.requests[request_id];
if (!request || !request['message']) {
return;
}
let status_code = error_code_obj['statusCode'];
let message = request['message'];
message['result'] = error_code_obj['result'];
message['errorCode'] = error_code_obj['errorCode'];
message['errorText'] = this.status_codes[status_code] || 'Unknown Error',
message['errorMessage'] = '[' + error_code_obj['errorName'] + '] ' + error_message;
message['statusCode'] = status_code;
this._returnResponseToApp(message);
};
GotapiInterface1.prototype._parseQueryStringFromUrl = function (url) {
let parts = url.split('?');
let q = parts[1];
if (q) {
return this._parseQueryString(q);
} else {
return { result: {} };
}
};
GotapiInterface1.prototype._parseQueryString = function (q) {
let p = {};
let kv_list = q.split('&');
let error = '';
kv_list.forEach((kv) => {
let pair = kv.split('=');
try {
p[pair[0]] = decodeURIComponent(pair[1]);
} catch (e) {
error = e.message;
}
});
if (error) {
return { result: {}, error: error };
} else {
return { result: p };
}
};
GotapiInterface1.prototype._returnResponseToApp = function (data) {
let request_id = data['request_id'];
let request = this.requests[request_id];
if (!request || !request['req'] || !request['res']) {
delete this.requests[request_id];
return;
}
let status_code = data['statusCode'];
if (data['result'] === 0) {
delete data['errorCode'];
delete data['errorText'];
delete data['errorMessage'];
if ('statusCode' in data) {
let sc = data['statusCode'];
//if (!sc || typeof (sc) !== 'number' || !parseInt(sc / 100, 10).toString().match(/^(2|4|5)$/) || !this.status_codes[sc]) {
if (!sc || typeof (sc) !== 'number' || !/^(2|4|5)\d{2}$/.test(sc.toString()) || !this.status_codes[sc]) {
data['result'] = 1;
data['errorCode'] = '1';
data['errorMessage'] = '[ERROR] Plug-In set an invalid HTTP status code: ' + data['statusCode'];
data['errorText'] = this.status_codes[500];
data['statusCode'] = 500;
}
} else {
data['statusCode'] = 200;
}
} else {
data['errorText'] = this.status_codes[status_code];
if (data['errorText']) {
if (!data['errorMessage']) {
data['errorMessage'] = data['errorText'];
}
} else {
data['result'] = 1;
data['errorCode'] = '1';
data['errorMessage'] = '[ERROR] Plug-In set an invalid HTTP status code: ' + data['statusCode'];
data['errorText'] = this.status_codes[500];
data['statusCode'] = 500;
}
}
let nonce = data['params']['nonce'];
let key = '';
if (data['_client']) {
key = data['_client']['key'];
}
if (nonce && key) {
data['hmac'] = mCrypto.createHmac('sha256', key).update(nonce).digest('hex');;
}
data['serviceId'] = ('params' in data) ? data['params']['serviceId'] : '';
// Delete the properties used internally
if (!(data['profile'] === 'authorization' && data['attribute'] === 'grant')) {
delete data['clientId'];
}
delete data['if_type'];
delete data['package'];
delete data['request_id'];
delete data['action'];
delete data['params'];
delete data['request_url'];
delete data['api'];
delete data['receiver'];
delete data['_client'];
delete data['method'];
delete data['_plugin_id_list'];
if (!this.status_codes[data['statusCode']]) {
data['result'] = 1;
data['errorCode'] = '1';
data['errorMessage'] = '[ERROR] Plug-In set an invalid HTTP status code: ' + data['statusCode'];
data['errorText'] = this.status_codes[500];
data['statusCode'] = 500;
}
let res = request['res'];
let req = request['req'];
let headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': req.headers['origin'] || '*'
};
res.writeHead(data['statusCode'], headers);
res.write(JSON.stringify(data));
res.end();
this._monitorOutgoing({ code: data['statusCode'], data: data, headers: headers });
data = null;
delete this.requests[request_id];
};
/* ------------------------------------------------------------------
* Method: postMessage(data)
* This method is just an alias of the `_returnResponseToApp()` method,
* which is exposed to the GotAPI Server.
* When this method is called by the GotAPI Server, this method will
* pass the message to the web app on the GotAPI-1 Interface.
* ---------------------------------------------------------------- */
GotapiInterface1.prototype.postMessage = GotapiInterface1.prototype._returnResponseToApp;
module.exports = GotapiInterface1;