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).
493 lines (463 loc) • 15.4 kB
JavaScript
/* ------------------------------------------------------------------
* node-gotapi - gotapi-plugin-util.js
*
* This module is used by Plug-Ins for communication with the GotAPI
* Server on the GotAPI-4 Interface.
*
* Copyright (c) 2017-2019, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2019-10-20
* ---------------------------------------------------------------- */
'use strict';
let mCrypto = require('crypto');
let mConsole = require('./console-util.js');
/* ------------------------------------------------------------------
* Constructor: GotapiPluginUtil(port)
* ---------------------------------------------------------------- */
let GotapiPluginUtil = function (port) {
// The MessagePort object which communicates with the GotAPI
// Server through the GotAPI-4 Interface
this.port = port;
// Client ID
this.client_id_map = {};
// Information of the Plug-In
this.info = null;
// Callback function which is called when a message comes from the GotAPI Server
this.onservicediscoverry = null;
this.onclinetid = null;
this.onaccesstoken = null;
this.onmessage = null;
//
this.requests = {};
this.error_code_map = require('./error-code.json');
};
/* ------------------------------------------------------------------
* Method: GotapiPluginUtil.init(message)
* ---------------------------------------------------------------- */
GotapiPluginUtil.prototype.init = function (info) {
let err = this._checkErrorInformationObject(info);
if (err) {
throw new Error(err);
} else {
this.info = info;
this.port.on('message', this._receiveMessage.bind(this));
}
};
GotapiPluginUtil.prototype._checkErrorInformationObject = function (info) {
let err = '';
if (typeof (info) !== 'object') {
return 'The `PluginInformation` object must be an Object object.';
}
let name = info['name'];
if (!name) {
return 'The `name` property is required in the `PluginInformation` object.';
} else if (typeof (name) !== 'string') {
return 'The value of the `name` property in the `PluginInformaton` object must be an string.';
}
let services = info['services'];
if (!services) {
err = 'The `services` property is requierd in the `PluginInformation` object.';
} else if (!Array.isArray(services)) {
err = 'The value of the `services` property in the `PluginInformation` object must be an Array object.';
} else if (services.length === 0) {
err = 'The number of elements in the `PluginInformation.services` must be grater than 0.'
}
let property_types = {
'serviceId': 'string',
'name': 'string',
'online': 'boolean',
'scopes': 'array',
'manufacturer': 'string',
'version': 'string',
'type': 'string'
};
let required_property_list = [
'serviceId',
'name',
'online',
'scopes'
];
for (let i = 0; i < services.length; i++) {
let s = services[i];
if (typeof (s) !== 'object') {
return 'The value of an element in the `PluginInformation.services` must be an Object object.';
}
for (let i = 0; i < required_property_list.length; i++) {
let name = required_property_list[i];
if (!(name in s)) {
return 'The `' + name + '` property is required in the `PluginInformation.services`.'
}
}
for (let name in property_types) {
if (!(name in s)) {
continue
}
let type = property_types[name];
let value = s[name];
if (type === 'string' || type === 'boolean') {
if (typeof (value) !== type) {
return 'The type of the `' + name + '` property in an element of the `PluginInformation.services` must be ' + type + '.';
}
} else if (type === 'array') {
if (!Array.isArray(value)) {
return 'The value of the `' + name + '` property in an element of the `PluginInformation.services` must be an Array object.';
} else if (value.length === 0) {
return 'The value of the `' + name + '` property in an element of the `PluginInformation.services` must be an Array object containing at least an element.'
}
}
}
}
return '';
};
GotapiPluginUtil.prototype._receiveMessage = function (message_orig) {
let message = JSON.parse(JSON.stringify(message_orig));
this.requests[message['requestCode']] = message;
// Plug-In Discovery (Service Discovery)
if (message.profile === 'networkServiceDiscovery') {
if (message.attribute === 'getNetworkServices') {
this._getNetworkServices(message);
}
// Plug-In Approval
} else if (message.profile === 'authorization') {
// Request for registration of application
if (message.attribute === 'createClient') {
this._createClient(message);
// Request for an access token
} else if (message.attribute === 'requestAccessToken') {
this._requestAccessToken(message);
}
// Plug-In functions
} else {
if (this.onmessage && typeof (this.onmessage) === 'function') {
try {
this.onmessage(message);
} catch (error) {
mConsole.error('[Plug-In ERROR]', error);
this._returnErrorToGotapiServer(
this.error_code_map['ERROR'],
`The Plug-In caused an error: ` + error.message,
message
);
}
} else {
this._returnErrorToGotapiServer(
this.error_code_map['ERROR'],
'The Plug-In forgets to attach a callback to reply messages.',
message
);
}
}
};
// Plug-In Discovery (Service Discovery)
GotapiPluginUtil.prototype._getNetworkServices = function (message) {
/*
* message = {
* receiver: "com.github.futomi.node-gotapi", // The application ID of the GotAPI Server
* requestCode: "xxxxxxxxxxxxxxxxxx", // A request code,
* api: "gotapi",
* profile: "networkServiceDiscovery",
* attribute: "getNetworkServices"
* }
*/
if (this.onservicediscoverry && typeof (this.onservicediscoverry) === 'function') {
this.onservicediscoverry(message);
} else {
message['result'] = 0;
message['services'] = JSON.parse(JSON.stringify(this.info.services));
this.port.postMessage(message);
let request_code = message['requestCode'];
if(this.requests[request_code]) {
delete this.requests[request_code];
}
}
};
/* ------------------------------------------------------------------
* Method: GotapiPluginUtil.returnServiceDiscovery(message);
* ---------------------------------------------------------------- */
GotapiPluginUtil.prototype.returnServiceDiscovery = function (message) {
let request_code = message['requestCode'];
let res = this.requests[request_code];
if (!res) {
return;
}
let services = [];
if (('services' in message) && Array.isArray(message['services'])) {
services = message['services'];
}
res['result'] = 0;
res['services'] = services;
this.port.postMessage(res);
delete this.requests[request_code];
};
// Plug-In Approval
GotapiPluginUtil.prototype._createClient = function (message) {
/*
* message = {
* receiver: "com.github.futomi.node-gotapi", // The application ID of the GotAPI Server
* requestCode: 1, // A request code,
* api: "gotapi",
* profile: "authorization",
* attribute: "createClient",
* package: "http://localhost:8080" // The origin of the web app
* }
*/
message['accept'] = true;
if (this.onclinetid && typeof (this.onclinetid) === 'function') {
this.onclinetid(message);
} else {
this.returnClientIdRequest(message);
}
};
/* ------------------------------------------------------------------
* Method: GotapiPluginUtil.returnClientIdRequest(message);
* ---------------------------------------------------------------- */
GotapiPluginUtil.prototype.returnClientIdRequest = function (message) {
let request_code = message['requestCode'];
let res = this.requests[request_code];
if (!res) {
return;
}
if (message && message['accept'] === true) {
// Create an client ID
let client_id = '';
client_id += mCrypto.randomBytes(8).toString('hex') + '_';
client_id += res.package + '_' + Date.now();
let client_id_sha256 = mCrypto.createHash('sha256');
client_id_sha256.update(client_id);
client_id = client_id_sha256.digest('hex');
this.client_id_map[client_id] = { client_id: client_id };
// Return the result
res['result'] = 0;
res['clientId'] = client_id;
this.port.postMessage(res);
} else {
let error_message = 'The Plug-In denied the request for a client ID.';
if (message['errorMessage']) {
error_message = message['errorMessage'];
}
this._returnErrorToGotapiServer(
this.error_code_map['NOT_AUTHORIZED'],
error_message,
res
);
}
delete this.requests[request_code];
};
// Plug-In Approval
GotapiPluginUtil.prototype._requestAccessToken = function (message) {
/*
* message = {
* receiver: "com.github.futomi.node-gotapi", // The application ID of the GotAPI Server
* requestCode: 1, // A request code,
* api: "gotapi",
* profile: "authorization",
* attribute: "requestAccessToken",
* package: "http://localhost:8080" // The origin of the web app
* clientId: "xxxxxxxxxxxxxxx"
* }
*/
// This is an web application identifirer
let app_id = message.package; // the web application id
let client_id = message.clientId;
let error_message = '';
if (!app_id) {
error_message = 'A package ID (application ID) is required.';
} else if (!client_id) {
error_message = 'A client ID is required.';
} else if (!this.client_id_map[client_id]) {
error_message = 'The specified cliend ID has not been registered or has been expired.'
}
if (error_message) {
// Return the result
this._returnErrorToGotapiServer(
this.error_code_map['NOT_AUTHORIZED'],
error_message,
message
);
} else {
message['accept'] = true;
if (this.onaccesstoken && typeof (this.onaccesstoken) === 'function') {
this.onaccesstoken(message);
} else {
this.returnAccessTokenRequest(message);
}
}
};
/* ------------------------------------------------------------------
* Method: GotapiPluginUtil.returnAccessTokenRequest(message);
* ---------------------------------------------------------------- */
GotapiPluginUtil.prototype.returnAccessTokenRequest = function (message) {
let request_code = message['requestCode'];
let res = this.requests[request_code];
if (!res) {
return;
}
if (message && message['accept'] === true) {
let client_id = res['clientId'];
// Create an access token
let access_token = '';
access_token += mCrypto.randomBytes(8).toString('hex') + '_';
access_token += client_id + '_';
access_token += Date.now();
let access_token_sha256 = mCrypto.createHash('sha256');
access_token_sha256.update(access_token);
access_token = access_token_sha256.digest('hex');
this.client_id_map[client_id]['accessToken'] = access_token;
// Return the result
res['result'] = 0;
res['accessToken'] = access_token;
this.port.postMessage(res);
} else {
let error_message = 'The Plug-In denied the request for an access token.';
if (message['errorMessage']) {
error_message = message['errorMessage'];
}
this._returnErrorToGotapiServer(
this.error_code_map['NOT_AUTHORIZED'],
error_message,
res
);
}
delete this.requests[request_code];
};
/* ------------------------------------------------------------------
* Method: GotapiPluginUtil.returnMessage(message);
* message = {
* requestCode: "xxxxxxxxxxxx",
* result: 401,
* data: null,
* errorMessage: ""
* }
* ---------------------------------------------------------------- */
GotapiPluginUtil.prototype.returnMessage = function (message) {
message['action'] = 'RESPONSE';
this._postMessage(message);
};
GotapiPluginUtil.prototype._postMessage = function (message) {
if (!message) {
return;
}
let request_code = message['requestCode'];
if (!request_code || !this.requests[request_code]) {
return;
}
let request = this.requests[request_code];
let res = JSON.parse(JSON.stringify(request));
res['action'] = message['action'];
if ('result' in message) {
let result = message['result'];
if (typeof (result) === 'string' && result.match(/^\d+$/)) {
result = parseInt(result, 10);
}
if (typeof (result) === 'number' && result % 1 === 0) {
res['result'] = result;
} else {
res['statusCode'] = 500;
res['errorMessage'] = 'An invalid result code was specified by the Plug-In: ' + message['result'].toString()
}
} else {
res['result'] = 0;
}
if (res['result'] !== 0) {
if ('errorCode' in message) {
let error_code = message['errorCode'];
if (typeof (error_code) !== 'string') {
error_code = error_code.toString();
}
res['errorCode'] = error_code;
} else {
res['errorCode'] = res['result'].toString();
}
if ('errorMessage' in message) {
let error_message = message['errorMessage'];
if (typeof (error_message) !== 'string') {
error_message = error_message.toString();
}
res['errorMessage'] = error_message;
} else {
res['errorMessage'] = '';
}
}
if ('statusCode' in message) {
let status_code = message['statusCode'];
if (typeof (status_code) === 'string' && status_code.match(/^\d+$/)) {
status_code = parseInt(status_code, 10);
}
if (typeof (status_code) === 'number') {
let status_type = parseInt(status_code / 100, 10);
if (res['result'] === 0) {
if (status_type === 2) {
res['statusCode'] = status_code;
} else {
res['statusCode'] = 200;
}
} else {
if (status_type.toString().match(/^(4|5)$/)) {
res['statusCode'] = status_code;
} else {
res['statusCode'] = 400;
}
}
} else {
res['result'] = 1;
res['errorCode'] = '1';
res['statusCode'] = 500;
res['errorMessage'] = 'The Plug-In module specified an invalid HTTP status code: ' + status_code.toString();
}
} else {
if (res['result'] === 0) {
res['statusCode'] = 200;
} else {
let ecode = message['errorCode'];
if (ecode && typeof (ecode) === 'number' && ecode % 1 === 0 && parseInt(ecode / 100).toString().match(/^(2|4|5)$/)) {
// This is for backward compatibility.
// The node-gotapi v0.2.2 and earlier used the `errorCode` for a HTTP status code.
// This work-around will be deleted sometime in the future.
res['statusCode'] = ecode;
} else {
res['statusCode'] = 400;
}
}
}
for (let k in message) {
if (!(k in res)) {
res[k] = message[k];
}
}
if (res['result'] === 0) {
delete res['errorCode'];
delete res['errorMessage'];
}
this.port.postMessage(res);
delete this.requests[request_code];
};
GotapiPluginUtil.prototype._returnErrorToGotapiServer = function (error_code_obj, message, data) {
data['result'] = error_code_obj['result'];
data['errorCode'] = error_code_obj['errorCode'];
data['errorMessage'] = '[' + error_code_obj['errorName'] + '] ' + message;
data['statusCode'] = error_code_obj['statusCode'];
this.port.postMessage(data);
};
/* ------------------------------------------------------------------
* Method: GotapiPluginUtil.pushMessage(message);
* message = {
* result: 0,
* data: 'something'
* }
* ---------------------------------------------------------------- */
GotapiPluginUtil.prototype.pushMessage = function (message) {
if (!message || typeof (message) !== 'object' || !('result' in message)) {
return;
}
let result = message['result'];
if (typeof (result) === 'string' && result.match(/^d+$/)) {
result = parseInt(result, 10);
}
if (typeof (result) !== 'number' || result % 1 !== 0) {
return;
}
let res = JSON.parse(JSON.stringify(message));
res['action'] = 'EVENT';
this.port.postMessage(res);
};
module.exports = GotapiPluginUtil;