/*
* (c) 2020 BlackBerry Limited. All rights reserved.
*/
;
(function() {
var cordovaExec = require('cordova/exec');
/**
* @class GDSocketResponse
* @classdesc This class encapsulates the response returned from the GDSocket class.
*
* @param {string} json The input data (formatted as JSON text) used to construct the
* response object.
*
* @param {string} payloadMessageType Optional parameter that represents the type of payload message of responseData property in "message" event.
* Possible values are "string" and "binary". "string" value is by default.
*
* @property {string} socketID The unique ID for the socket connection that generated this response.
*
* @property {string} responseType This value is used to distinguish what action triggered this response.
* Valid values are:
* <ul>
* <li>open - The socket was just successfully opened.</li>
* <li>message - A new message was received from the server. The responseData property will be
* populated with the data from the server as String or Uint8Array depending on payloadMessageType.
* If socket is not closed connections is kept alive waiting for new data coming.</li>
* <li>error - A socket error occurred. The responseData may or may not be populated with a
* description of the error.</li>
* <li>close - The socket connection was closed.</li>
* </ul>
*
* @property {string} responseData This field will be populated with data from the server if the
* response contained data what was intended to be processed by the client.
*
* @return {GDSocketResponse}
*
* @example
* // This object is used by GDSocket.parseSocketResponse() method and is not used directly
*/
var GDSocketResponse = function(json, payloadMessageType) {
var socketID = null,
responseType = null,
responseData = null;
try {
var obj = JSON.parse(unescape(json));
socketID = obj.socketID;
responseType = obj.responseType;
if (responseType === 'message') {
var payload;
// obj.responseData is always base64 string that is passed from Java layer to JS layer
// by requirements we want to convert it to String or Uint8Array
if (payloadMessageType === 'binary') {
payload = base64ToByteArray(obj.responseData);
} else if (payloadMessageType === 'string' && streamMode) {
payload = atob(obj.responseData);
} else if (payloadMessageType === 'string' && !streamMode) {
payload = obj.responseData;
} else {
responseType = 'error';
payload = 'Not supported payloadMessageType. Available are "string" and "binary"';
}
responseData = payload;
} else {
responseData = obj.responseData;
}
function base64ToByteArray(base64String) {
var sliceSize = 4096,
byteCharacters = atob(base64String),
bytesLength = byteCharacters.length,
slicesCount = Math.ceil(bytesLength / sliceSize),
byteArrays = new Array(slicesCount);
for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
var begin = sliceIndex * sliceSize,
end = Math.min(begin + sliceSize, bytesLength),
bytes = new Array(end - begin);
for (var offset = begin, i = 0; offset < end; ++i, ++offset) {
bytes[i] = byteCharacters[offset].charCodeAt(0);
}
byteArrays[sliceIndex] = new Uint8Array(bytes);
}
return byteArrays;
}
} catch (e) {
// in some cases we might get an exception here:
// Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
// It depends on WebView implementations.
// Anyway base64 string is correctly decoded even we get this error
}
Object.defineProperties(this, {
'socketID': {
get: function() {
return socketID;
}
},
'responseType': {
get: function() {
return responseType;
}
},
'responseData': {
get: function() {
return responseData;
}
},
'toString': {
value: function() {
return '[object GDSocketResponse]';
}
}
});
};
Object.defineProperty(GDSocketResponse, 'toString', {
value: function() {
return 'function GDSocketResponse() { [native code] }';
}
})
Object.preventExtensions(GDSocketResponse);
/**
* @class GDSocket
* @classdesc Implements the secure Socket communications APIs.
*
* @property {string} url The address of the server. Can be either an Internet Protocol
* address (IP address, for example "192.168.1.10"), or a fully qualified domain name
* (for example "www.example.com").
*
* @property {number} port Number of the server port to which the socket will connect.
*
* @property {boolean} useSSL This value determines whether or not to use SSL/TLS security.
*
* @property {string} payloadMessageType Optional parameter that represents the type of payload message of responseData property in "message" event.
* Possible values are "string" and "binary". "string" value is by default.
*
* @property {boolean} disableHostVerification Disable host name verification, when
* making an HTTPS request. Host name verification is an SSL/TLS security option.
*
* @property {boolean} disablePeerVerification Disable certificate authenticity verification,
* when making an SSL/TLS connection. Authenticity verification is an SSL/TLS security option.
*
* @property {function} onSocketResponse This function is the callback handler that is called
* whenever a response is returned from the socket connection. This function should check
* the value of the responseType returned and determine the required action to take. If the
* responseType = "open", then the socketID returned in the response should be used to send
* data in subsequent calls for this socket connection (see GDSocket.send). NOTE: This
* function is required to be a non-null value.
*
* @property {function} onSocketError This function is the callback handler that is called
* whenever a socket error occurs. This function should check the value of the responseType
* returned and determine the required action to take.
*/
var GDSocket = function() {
var url = null,
port = -1,
useSSL = false,
disableHostVerification = false,
disablePeerVerification = false,
onSocketResponse = null,
onSocketError = null;
Object.defineProperties(this, {
'url': {
get: function() {
return url;
},
set: function(socketUrl) {
url = socketUrl;
}
},
'port': {
get: function() {
return port;
},
set: function(socketPort) {
port = socketPort;
}
},
'useSSL': {
get: function() {
return useSSL;
},
set: function(socketUseSSL) {
useSSL = socketUseSSL;
}
},
'payloadMessageType': {
get: function() {
return payloadMessageType;
},
set: function(payloadMsgType) {
payloadMessageType = payloadMsgType;
}
},
'streamMode': {
get: function() {
return streamMode;
},
set: function(isStream) {
streamMode = isStream;
}
},
'disableHostVerification': {
get: function() {
return disableHostVerification;
},
set: function(socketShouldDisableHostVerification) {
disableHostVerification = socketShouldDisableHostVerification;
}
},
'disablePeerVerification': {
get: function() {
return disablePeerVerification;
},
set: function(socketShouldDisablePeerVerification) {
disablePeerVerification = socketShouldDisablePeerVerification;
}
},
'onSocketResponse': {
get: function() {
return onSocketResponse;
},
set: function(callback) {
onSocketResponse = callback;
}
},
'onSocketError': {
get: function() {
return onSocketError;
},
set: function(callback) {
onSocketError = callback;
}
},
'toString': {
value: function() {
return '[object GDSocket]';
}
}
})
};
Object.defineProperty(GDSocket, 'toString', {
value: function() {
return 'function GDSocket() { [native code] }';
}
});
Object.preventExtensions(GDSocket);
// ***** BEGIN: MODULE METHOD DEFINITIONS - GDSocket *****
/**
* @function GDSocket#createSocket
*
* @description Call this function to create a socket and set the main parameters. NOTE: This
* funtion only initializes the socket parameters; it does not initiate data transfer nor does
* it make the initial socket connection (see GDSocket.connect).
*
* @param {string} url The address of the server. Can be either an Internet Protocol
* address (IP address, for example "192.168.1.10"), or a fully qualified domain name
* (for example "www.example.com").
*
* @param {number} port Number of the server port to which the socket will connect.
*
* @param {boolean} useSSL his value determines whether or not to use SSL/TLS security.
*
* @param {string} payloadMessageType Optional parameter that represents the type of payload message of responseData property in "message" event.
* Possible values are "string" and "binary". "string" value is by default.
*
* @param {boolean} streamMode Optional parameter that indicates if socket is created in stream mode. true value is by default.
*
* @return {GDSocket}
*
* @example
* See the example below (it is added to GDSocket.send() method).
*/
GDSocket.prototype.createSocket = function(url, port, useSSL, payloadMessageType, streamMode) {
var result = new GDSocket();
result.url = url;
result.port = port;
result.useSSL = useSSL;
result.payloadMessageType = payloadMessageType || 'string';
result.streamMode = streamMode === false ? streamMode : true;
return result;
};
/**
* @function GDSocket#connect
*
* @description Open a new socket connection.
*
* @return {GDSocketResponse} A socket response object in JSON format. The result should be
* parsed and saved as a GDSocketResponse object in the callback handler. If the connection
* was successful then the response object will be initialize with a socketID property that
* can be used to send data using this socket connection (see GDSocket.send). Since this is
* an asynchronous call, the response will be returned via the onSocketResponse callback or
* the onSocketError callback (whichever is applicable).
*
* @example
*
* var mySocketStreamBinary = function () {
* if(console)
* console.log('>> Socket');
*
* var url = '10.0.2.2'; // this ip is the ip used by the android studio emulator to connect to it's host.
* var port = '11511';
* var useSSL = false;
*
* var aSocket = window.plugins.GDSocket.createSocket(url, port, useSSL, 'binary'); // payloadMessageType is 'binary' in this case
*
* aSocket.onSocketResponse = function (obj) {
* var socketResponse = window.plugins.GDSocket.parseSocketResponse(obj);
* if(console)
* console.log (socketResponse.responseType)
*
* switch (socketResponse.responseType) {
* case 'open':
* break;
*
* case 'message':
* if(console)
* console.log('message:' , socketResponse.responseData); // Uint8Array data is going to be here because of 'binary' value of payloadMessageType
* break;
*
* case 'error':
* console.log('Received an error status from the socket connection.');
* break;
*
* case 'close':
* console.log('Socket connection closed successfully.');
* break;
*
* default:
* console.log('Unknown Socket response type: ' + socketResponse.responseType);
* }
* };
* // error
* aSocket.onSocketError = function (error) {
* if(console)
* console.log('The socket connection failed: ' + error);
* };
*
* // connect!
* aSocket.connect();
* };
*
* var mySocketStreamString = function () {
* if(console)
* console.log('>> Socket');
*
* var url = '10.0.2.2'; // this ip is the ip used by the android studio emulator to connect to it's host.
* var port = '11511';
* var useSSL = false;
*
* var aSocket = window.plugins.GDSocket.createSocket(url, port, useSSL); // payloadMessageType is 'string' in this case by default
*
* aSocket.onSocketResponse = function (obj) {
* var socketResponse = window.plugins.GDSocket.parseSocketResponse(obj);
* if(console)
* console.log (socketResponse.responseType)
*
* switch (socketResponse.responseType) {
* case 'open':
* break;
*
* case 'message':
* if(console)
* console.log('message:' , socketResponse.responseData); // String data is going to be here because 'string' is a default value of payloadMessageType
* break;
*
* case 'error':
* console.log('Received an error status from the socket connection.');
* break;
*
* case 'close':
* console.log('Socket connection closed successfully.');
* break;
*
* default:
* console.log('Unknown Socket response type: ' + socketResponse.responseType);
* }
* };
* // error
* aSocket.onSocketError = function (error) {
* if(console)
* console.log('The socket connection failed: ' + error);
* };
*
* // connect!
* aSocket.connect();
* };
*/
GDSocket.prototype.connect = function() {
// Make sure that the response callback handler is not null.
if (this.onSocketResponse === null) {
throw new Error("onSocketResponse callback handler for GDSocket object is null.");
}
var lUseSSL = (this.useSSL === true) ? "true" : "false",
lHost = (this.disableHostVerification === true) ? "true" : "false",
lPeer = (this.disablePeerVerification === true) ? "true" : "false",
lSteam = (this.streamMode === true) ? "true" : "false",
lType = (this.payloadMessageType === "binary") ? "binary" : "string",
parms = [this.url, this.port.toString(), lUseSSL, lHost, lPeer, lSteam, lType];
cordovaExec(this.onSocketResponse, this.onSocketError, "GDSocket", "connect", parms);
};
/**
* @function GDSocket#send
*
* @description Call this function to send data using the open socket connection. send() method works asynchronously.
* This means that good place to close the connection is when data is received in "message" event.
* If socket is not closed connections is kept alive waiting for new data coming.
*
* @param {string | UInt8Array} data Optional parameters that indicates what data will be transmitted using the open socket.
*
* @param {string} socketID The identifier for the open socket connection. This value
* is returned from a successful call to GDSocket.connect.
*
* @example
* function mySocketSend(){
* var url = "httpbin.org";
* var aSocket = window.plugins.GDSocket.createSocket(url, 80, true, 'string', false); // payloadMessageType is by default 'string' in this case
*
* aSocket.onSocketResponse = function (obj) {
* var socketResponse = aSocket.parseSocketResponse(obj);
*
* switch (socketResponse.responseType) {
* case "open":
* var httpRequest = "GET / HTTP/1.1\r\n" +
* "Host: httpbin.org:80\r\n" +
* "Connection: close\r\n" +
* "\r\n";
*
* aSocket.send(socketResponse.socketID, httpRequest);
* break;
*
* case "message":
* var httpRespObj = aSocket.parseHttpResponse(socketResponse.responseData);
* console.log(httpRespObj.status)
* console.log(httpRespObj.statusCode)
* console.log(httpRespObj.responseHeaders)
* console.log(httpRespObj.responseBody)
* aSocket.close(socketResponse.socketID);
* break;
*
* case "error":
* // handle error
* break;
*
* case "close":
* console.log("Socket connection closed");
* break;
*
* default:
* // handle default case
* }
* };
*
* // error
* aSocket.onSocketError = function (error) {
* // handle error
* };
*
* // connect!
* aSocket.connect();
*
* }
*/
GDSocket.prototype.send = function(socketID, data) {
if (socketID === null) {
throw new Error("Null socketID passed to GDSocket.send.");
}
if (!data) {
data = "";
} else if (data.constructor.name === "Uint8Array") {
data = u8ToBase64(data);
}
var params = [socketID, data];
function u8ToBase64(u8) {
return btoa(String.fromCharCode.apply(null, u8));
}
cordovaExec(this.onSocketResponse, this.onSocketError, "GDSocket", "send", params);
};
/**
* @function GDSocket#close
*
* @description Call this function to close the socket connection. send() and close() methods work asynchronously.
* This means that good place to close the connection is when data is received in "message" event.
*
* @param {string} socketID The identifier for the open socket connection. This value
* is returned from a successful call to GDSocket.connect.
*
* @example
* See the example below (it is added to GDSocket.send() method).
*/
GDSocket.prototype.close = function(socketID) {
if (socketID === null) {
throw new Error("Null socketID passed to GDSocket.close.");
}
var parms = [socketID];
cordovaExec(this.onSocketResponse, this.onSocketError, "GDSocket", "close", parms);
};
/**
* @function GDSocket#parseSocketResponse
*
* @description Call this function to transform the socket response text into a
* GDSocketResponse object.
*
* @param {string} responseText A string representing the socket response text.
*
* @return {GDSocketResponse} The socket response object.
*
* @example
* See the example below (it is added to GDSocket.send() method).
*/
GDSocket.prototype.parseSocketResponse = function(responseText) {
return new GDSocketResponse(responseText, this.payloadMessageType);
};
/**
* @function GDSocket#parseHttpResponse
*
* @description Call this function to transform the HTTP response text from GDSocket into an object. Applicable only in case of 'string' payloadMessageType.
* Following properties are available:
* <ul>
* <li>status - status string, for example, 'HTTP/1.1 200 OK'.</li>
* <li>statusCode - status code number, for example, 200.</li>
* <li>responseHeaders - an array of response headers,
* each array item is itself an object where key is response header name and value is response header value.</li>
* <li>responseBody - response body, depending on content type response body will be different.</li>
* </ul>
*
*
* @param {string} responseText A string representing the HTTP response text from GDSocket.
*
* @return {Object} The socket HTTP response object.
*
* @example
* See the example below (it is added to GDSocket.send() method).
*/
GDSocket.prototype.parseHttpResponse = function(responseText) {
if (typeof responseText !== "string") {
throw 'Only "string" payload message type can be parsed';
}
var headersPartOfResponse = responseText.slice(0, responseText.indexOf("\n\r")),
responseBody = responseText.slice(responseText.indexOf("\n\r"), responseText.length).trim(),
splitedHeaders = headersPartOfResponse.split("\n")
.filter(function(item) { return item.length > 1; }),
status = splitedHeaders.shift(),
statusCode = parseInt(status.split(" ")[1]),
responseHeaders = splitedHeaders.map(function(item) {
var splited = item.split(':'),
res = {};
res[splited[0]] = splited[1].trim();
return res;
});
return {
status: status,
statusCode: statusCode,
responseHeaders: responseHeaders,
responseBody: responseBody
}
}
// ***** END: MODULE METHOD DEFINITIONS - GDSocket *****
// hide functions implementation in web inspector
for (protoFunction in GDSocket.prototype) {
if (GDSocket.prototype.hasOwnProperty(protoFunction)) {
// Checking, if function property 'name' is configurable
// (for old browser, which has pre-ES2015 implementation(Android 5.0) function name property isn't configurable)
var objProtoProperty = GDSocket.prototype[protoFunction],
isFuncNamePropConfigurable = Object.getOwnPropertyDescriptor(objProtoProperty, 'name').configurable;
if (isFuncNamePropConfigurable) {
Object.defineProperty(GDSocket.prototype[protoFunction],
'name', {
value: protoFunction,
configurable: false
}
);
}
Object.defineProperty(GDSocket.prototype[protoFunction],
'toString', {
value: function() {
var funcName = this.name || protoFunction;
return 'function ' + funcName + '() { [native code] }';
},
writable: false,
configurable: false
});
}
}
Object.preventExtensions(GDSocket.prototype);
var gdSocket = new GDSocket();
Object.preventExtensions(gdSocket);
// Install the plugin.
module.exports = gdSocket;
}()); // End the Module Definition.
//************************************************************************************************