Source: cordova-plugin-bbd-socket/assets/www/android/GDSocket.js

/*
 * (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.
//************************************************************************************************