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).
456 lines (424 loc) • 13.7 kB
JavaScript
/* ------------------------------------------------------------------
* node-gotapi - gotapi-client.js
*
* Copyright (c) 2017-2018, Futomi Hatano, All rights reserved.
* Released under the MIT license
* Date: 2018-12-23
* ---------------------------------------------------------------- */
'use strict';
/*-------------------------------------------------------------------
* Constructor: GotapiClient()
* ---------------------------------------------------------------- */
let GotapiClient = function() {
let is_ssl = window.location.protocol.match(/^https/) ? true : false;
let hostname = window.location.hostname;
let port = is_ssl ? 4036 : 4035;
this.if1_base_url = (is_ssl ? 'https' : 'http') + '://' + hostname + ':' + port;
this.if5_base_url = (is_ssl ? 'wss' : 'ws') + '://' + hostname + ':' + port + '/gotapi/websocket';
this.key = '';
this.client_id = '';
this.access_token = '';
this.services = [];
this.onmessage = null;
this.ws = null;
// For Debug
this.oncommunication = null;
};
/*-------------------------------------------------------------------
* Plulic Method: disconnect()
* ---------------------------------------------------------------- */
GotapiClient.prototype.disconnect = function() {
if(this.ws) {
this.ws.close();
this.ws = null;
}
this.key = '';
this.client_id = '';
this.access_token = '';
this.services = [];
this.onmessage = null;
this.oncommunication = null;
};
/*-------------------------------------------------------------------
* Plulic Method: connect()
* ---------------------------------------------------------------- */
GotapiClient.prototype.connect = function() {
let promise = new Promise((resolve, reject) => {
if(this._prepareWebCrypto()) {
this._requestAvailabilityApi().then((key) => {
this.key = key;
return this._requestAppAuthorization();
}).then((client_id) => {
this.client_id = client_id;
return this._requestAccessToken();
}).then((access_token) => {
this.access_token = access_token;
return this.requestServiceDiscovery();
}).then((services) => {
this.services = services;
return this._establishIf5Connection();
}).then(() => {
resolve(this.services);
}).catch((error) => {
reject(error);
});
} else {
reject(new Error('Your browser does not support the Web Cryptography API.'));
}
});
return promise;
};
GotapiClient.prototype._prepareWebCrypto = function() {
if(window.crypto) {
if(!window.crypto.subtle) {
if(window.crypto.webkitSubtle) {
window.crypto.subtle = window.crypto.webkitSubtle;
}
}
}
return (window.crypto && window.crypto.subtle) ? true : false;
};
GotapiClient.prototype._requestAvailabilityApi = function() {
let promise = new Promise((resolve, reject) => {
let key = this._createRandomString();
//let url = this.if1_base_url + '/gotapi/availability?key=' + key;
let url = this.if1_base_url + '/gotapi/availability?' + this._createQueryString({key: key});
this._httpRequest('GET', url).then((o) => {
resolve(key);
}).catch((error) => {
reject(new Error('[AVAILABILITY ERROR] ' + error.message));
});
});
return promise;
};
GotapiClient.prototype._createQueryString = function(o) {
let pair_list = [];
Object.keys(o).forEach((k) => {
pair_list.push(k + '=' + encodeURIComponent(o[k]));
});
return pair_list.join('&');
};
GotapiClient.prototype._createRandomString = function(len) {
if(!len) {
len = 32;
}
let key_array = new Uint8Array(len);
window.crypto.getRandomValues(key_array);
let key_hex_list = [];
for (let i=0; i<key_array.length; i++) {
let hex = ('0' + key_array[i].toString(16)).slice(-2)
key_hex_list.push(hex);
}
let key = key_hex_list.join('');
return key;
};
GotapiClient.prototype._isOnCommunicationSet = function() {
if(this.oncommunication && typeof(this.oncommunication) === 'function') {
return true;
} else {
false;
}
};
GotapiClient.prototype._httpRequest = function(method, url, data) {
let promise = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.onload = () => {
let o = xhr.response;
if(this._isOnCommunicationSet()) {
this.oncommunication({if:1, dir:2, body:JSON.stringify(o)});
}
if(xhr.status >= 200 && xhr.status < 300) {
resolve(o);
} else if(o && o.result !== 0) {
let e = new Error(xhr.status + ' ' + xhr.statusText + ' (' + o.errorMessage + ')');
for(let k in o) {
if(!(k in e)) {
e[k] = o[k];
}
}
reject(e);
} else {
reject(new Error(xhr.status + ' ' + xhr.statusText));
}
};
xhr.onerror = (error) => {
reject(new Error('HTTP connection was refused.'));
};
xhr.open(method, url);
if(data) {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
xhr.responseType = 'json';
xhr.send(data);
if(this._isOnCommunicationSet()) {
this.oncommunication({if:1, dir:1, method:method, url: url});
}
});
return promise;
};
/*-------------------------------------------------------------------
* Request application authorization (Grant)
* ---------------------------------------------------------------- */
GotapiClient.prototype._requestAppAuthorization = function() {
let promise = new Promise((resolve, reject) => {
let nonce = this._createRandomString();
//let url = this.if1_base_url + '/gotapi/authorization/grant?nonce=' + nonce;
let url = this.if1_base_url + '/gotapi/authorization/grant?' + this._createQueryString({nonce: nonce});
let res = null;
this._httpRequest('GET', url).then((o) => {
res = o;
return this._generateHmac(this.key, nonce);
}).then((hmac) => {
if(res.hmac === hmac) {
resolve(res.clientId);
} else {
reject(new Error('[AUTH ERROR] The response is not trusted.'));
}
}).catch((error) => {
reject(new Error('[AUTH ERROR] ' + error.message));
});
});
return promise;
};
GotapiClient.prototype._generateHmac = function(key, nonce) {
let promise = new Promise((resolve, reject) => {
let key_buf = this._createArrayBufferFromString(key);
let nonce_buf = this._createArrayBufferFromString(nonce);
window.crypto.subtle.importKey(
'raw', key_buf, {name:'HMAC', hash:{name:'SHA-256'}}, true, ['sign', 'verify']
).then((key_obj) => {
return window.crypto.subtle.sign({name:'HMAC', hash:{name:'SHA-256'}}, key_obj, nonce_buf);
}).then((signature) => {
let view = new Uint8Array(signature);
let len = signature.byteLength;
let hmac = '';
for(let i=0; i<len; i++) {
let v = view[i].toString(16);
hmac += ('0' + v).slice(-2);
}
resolve(hmac);
}).catch((e) => {
reject(e);
});
});
return promise;
};
GotapiClient.prototype._createArrayBufferFromString = function(string) {
let len = string.length;
let view = new Uint8Array(len);
for(let i=0; i<len; i++) {
view[i] = string.codePointAt(i);
}
return view.buffer;
}
/*-------------------------------------------------------------------
* Request Access Token
* ---------------------------------------------------------------- */
GotapiClient.prototype._requestAccessToken = function() {
let promise = new Promise((resolve, reject) => {
let nonce = this._createRandomString();
//let url = this.if1_base_url + '/gotapi/authorization/accesstoken?clientId=' + this.client_id + '&scope=all&nonce=' + nonce;
// The url parameter `scope` is a dummy parameter here.
// Though the GotAPI spec defines the `scope` parameter,
// it is not necessary for the node-gotapi because it is
// not suitable for likely use cases of the node-gotapi.
// The `scope` is designed for smartphone users to permit
// an app to use the scope (functions) which the app want
// to use.
let url = this.if1_base_url + '/gotapi/authorization/accesstoken?';
url += this._createQueryString({
clientId : this.client_id,
scope : 'all',
nonce : nonce
});
let res = null;
this._httpRequest('GET', url).then((o) => {
res = o;
return this._generateHmac(this.key, nonce);
}).then((hmac) => {
if(res.hmac === hmac) {
resolve(res.accessToken);
} else {
reject(new Error('[TOKEN ERROR] The response is not trusted.'));
}
}).catch((error) => {
reject(new Error('[TOKEN ERROR] ' + error.message));
});
});
return promise;
};
/*-------------------------------------------------------------------
* Request Service Discovery
* ---------------------------------------------------------------- */
/*-------------------------------------------------------------------
* Plulic Method: requestServiceDiscovery()
* ---------------------------------------------------------------- */
GotapiClient.prototype.requestServiceDiscovery = function() {
let promise = new Promise((resolve, reject) => {
let nonce = this._createRandomString();
let url = this.if1_base_url + '/gotapi/servicediscovery?';
url += this._createQueryString({
accessToken : this.access_token,
nonce : nonce
});
let res = null;
this._httpRequest('GET', url).then((o) => {
res = o;
return this._generateHmac(this.key, nonce);
}).then((hmac) => {
if(res.hmac === hmac) {
resolve(res.services);
} else {
reject(new Error('[DISCOVERY ERROR] The response is not trusted.'));
}
}).catch((error) => {
reject(new Error('[DISCOVERY ERROR] ' + error.message));
});
});
return promise;
};
/*-------------------------------------------------------------------
* Establish a WebSocket connection
* ---------------------------------------------------------------- */
GotapiClient.prototype._establishIf5Connection = function() {
let promise = new Promise((resolve, reject) => {
let ws = new WebSocket(this.if5_base_url);
ws.onopen = () => {
let data = JSON.stringify({
accessToken: this.access_token
});
ws.send(data);
if(this._isOnCommunicationSet()) {
this.oncommunication({if:5, dir:1, body:data});
}
};
ws.onclose = (event) => {
//
};
ws.onerror = (error) => {
reject(new Error('Failed to establish a WebSocket connection.'));
};
ws.onmessage = (res) => {
resolve();
ws.onmessage = (res) => {
if(this.onmessage && typeof(this.onmessage) === 'function') {
let o = null;
try {
o = JSON.parse(res.data);
} catch(e) {}
if(o) {
if(o['result'] === 0) {
this.onmessage(o);
} else {
let e = new Error(o['errorCode'] + ' ' + o['errorText'] + ' (' + o['errorMessage'] + ')');
for(let k in o) {
if(!(k in e)) {
e[k] = o[k];
}
}
this.onmessage(e);
}
} else {
var error_message = '500 Internal Server Error (JSON parse error.)';
let e = new Error(error_message);
e['result'] = 500;
e['errorCode'] = 500;
e['errorText'] = 'Internal Server Error';
e['errorMessage'] = error_message;
this.onmessage(e);
}
}
if(this._isOnCommunicationSet()) {
this.oncommunication({if:5, dir:2, body:res.data});
}
};
};
this.ws = ws;
});
return promise;
};
/*-------------------------------------------------------------------
* Plulic Method: request(params)
* ---------------------------------------------------------------- */
GotapiClient.prototype.request = function(params) {
let promise = new Promise((resolve, reject) => {
let error = this._checkArgumentsForRequest(params);
if(error) {
reject(new Error(error));
}
let nonce = this._createRandomString();
let url = this.if1_base_url + '/gotapi/';
url += params['profile'] + '/' + params['attribute'];
let q = {
accessToken : this.access_token,
nonce : nonce
};
for(let k in params) {
if(!k.match(/^(profile|attribute|method)$/)) {
q[k] = params[k];
}
}
let qstring = this._createQueryString(q);
let method = params['method'].toLowerCase();
let data = null;
if(method === 'post' || method === 'put') {
data = qstring;
} else {
if(q) {
url += '?' + qstring;
}
}
let res = null;
this._httpRequest(params['method'], url, data).then((o) => {
res = o;
return this._generateHmac(this.key, nonce);
}).then((hmac) => {
if(res.hmac === hmac) {
resolve(res);
} else {
reject(new Error('[AUTH ERROR] The response is not trusted.'));
}
}).catch((error) => {
reject(error);
});
});
return promise;
};
GotapiClient.prototype._checkArgumentsForRequest = function(params) {
if(!params) {
return 'The argument `params` is required.';
} else if(typeof(params) !== 'object') {
return 'The argument `params` must be an Object object.';
}
let method = params['method'];
if(!method) {
return 'The property `method` is required.';
} else if(typeof(method) !== 'string' || !method.match(/^(get|post|put|delete)$/i)) {
return 'The value of the property `method` is invalid.';
}
let profile = params['profile'];
if(!profile) {
return 'The property `profile` is required.';
} else if(typeof(profile) !== 'string' || profile.match(/[^a-zA-Z0-9\-\_\.]/)) {
return 'The value of the property `profile` is invalid.';
}
let attribute = params['attribute'];
if(attribute) {
if(typeof(attribute) !== 'string' || attribute.match(/[^a-zA-Z0-9\-\_\.]/)) {
return 'The value of the property `attribute` is invalid.';
}
}
let service_id = params['serviceId'];
if(!service_id) {
return 'The property `serviceId` is required.';
} else if(typeof(service_id) !== 'string' || service_id.match(/[^a-zA-Z0-9\-\_\.\:\*\+]/)) {
return 'The value of the property `serviceId` is invalid.';
}
for(let k in params) {
let v = params[k];
if(!k.match(/^[^\d][a-zA-Z0-9\-\_]*/)) {
return 'The property name in the `params` is invalid.';
}
}
return '';
}