THRIFT-2355 Add SSL and Web Socket Support to Node and JavaScript
Patch: Randy Abernethy
diff --git a/lib/js/Gruntfile.js b/lib/js/Gruntfile.js
index 9a8bb0d..321063f 100644
--- a/lib/js/Gruntfile.js
+++ b/lib/js/Gruntfile.js
@@ -40,8 +40,14 @@
InstallThriftJS: {
command: 'mkdir test/build; mkdir test/build/js; cp src/thrift.js test/build/js/thrift.js'
},
+ InstallThriftNodeJSDep: {
+ command: 'cd ../nodejs; npm install'
+ },
ThriftGen: {
command: 'thrift -gen js -gen js:node -o test ../../test/ThriftTest.thrift'
+ },
+ ThriftGenJQ: {
+ command: 'thrift -gen js:jquery -gen js:node -o test ../../test/ThriftTest.thrift'
}
},
external_daemon: {
@@ -50,19 +56,73 @@
startCheck: function(stdout, stderr) {
return (/Thrift Server running on port/).test(stdout);
},
- nodeSpawnOptions: {cwd: "test"}
+ nodeSpawnOptions: {
+ cwd: "test",
+ env: {NODE_PATH: "../../nodejs/lib:../../nodejs/node_modules"}
+ }
},
cmd: "node",
args: ["server_http.js"]
+ },
+ ThriftTestServer_TLS: {
+ options: {
+ startCheck: function(stdout, stderr) {
+ return (/Thrift Server running on port/).test(stdout);
+ },
+ nodeSpawnOptions: {
+ cwd: "test",
+ env: {NODE_PATH: "../../nodejs/lib:../../nodejs/node_modules"}
+ }
+ },
+ cmd: "node",
+ args: ["server_https.js"]
}
},
qunit: {
- all: {
+ ThriftJS: {
options: {
urls: [
'http://localhost:8088/test-nojq.html'
]
}
+ },
+ ThriftJSJQ: {
+ options: {
+ urls: [
+ 'http://localhost:8088/test.html'
+ ]
+ }
+ },
+ ThriftWS: {
+ options: {
+ urls: [
+ 'http://localhost:8088/testws.html'
+ ]
+ }
+ },
+ ThriftJS_TLS: {
+ options: {
+ '--ignore-ssl-errors': true,
+ urls: [
+ 'https://localhost:8089/test-nojq.html'
+ ]
+ }
+ },
+ ThriftJSJQ_TLS: {
+ options: {
+ '--ignore-ssl-errors': true,
+ urls: [
+ 'https://localhost:8089/test.html'
+ ]
+ }
+ },
+ ThriftWS_TLS: {
+ options: {
+ '--ignore-ssl-errors': true,
+ urls: [
+ 'https://localhost:8089/testws.html'
+ ]
+ }
}
},
jshint: {
@@ -87,6 +147,15 @@
grunt.loadNpmTasks('grunt-external-daemon');
grunt.loadNpmTasks('grunt-shell');
- grunt.registerTask('test', ['jshint', 'shell', 'external_daemon', 'qunit']);
- grunt.registerTask('default', ['jshint', 'shell', 'external_daemon', 'qunit', 'concat', 'uglify', 'jsdoc']);
+ grunt.registerTask('test', ['jshint', 'shell:InstallThriftJS', 'shell:InstallThriftNodeJSDep', 'shell:ThriftGen',
+ 'external_daemon:ThriftTestServer', 'external_daemon:ThriftTestServer_TLS',
+ 'qunit:ThriftJS', 'qunit:ThriftJS_TLS',
+ 'shell:ThriftGenJQ', 'qunit:ThriftJSJQ', 'qunit:ThriftJSJQ_TLS'
+ ]);
+ grunt.registerTask('default', ['jshint', 'shell:InstallThriftJS', 'shell:InstallThriftNodeJSDep', 'shell:ThriftGen',
+ 'external_daemon:ThriftTestServer', 'external_daemon:ThriftTestServer_TLS',
+ 'qunit:ThriftJS', 'qunit:ThriftJS_TLS',
+ 'shell:ThriftGenJQ', 'qunit:ThriftJSJQ', 'qunit:ThriftJSJQ_TLS',
+ 'concat', 'uglify', 'jsdoc'
+ ]);
};
diff --git a/lib/js/README b/lib/js/README
index 971f6da..07b188b 100644
--- a/lib/js/README
+++ b/lib/js/README
@@ -1,7 +1,8 @@
Thrift Javascript Library
=========================
This browser based Apache Thrift implementation supports
-RPC clients using the JSON protocol over Http[s] with XHR.
+RPC clients using the JSON protocol over Http[s] with XHR
+and WebSocket.
License
-------
@@ -22,13 +23,44 @@
specific language governing permissions and limitations
under the License.
-Example
--------
+Grunt Build
+------------
+The is the base directory for the Apache Thrift JavaScript
+library. This directory contains a Gruntfile.js and a
+package.json. Many of the build and test tools used here
+require a recent version of Node.js to be installed. To
+install the support files for the Grunt build tool execute
+the command:
+ $ npm install
+This reads the package.json and pulls in the appropriate
+sources from the internet. To build the JavaScript branch
+of Apache Thrift execute the command:
+ $ grunt
+This runs the grunt build tool, linting all of the source
+files, setting up and running the tests, concatenating and
+minifying the main libraries and generating the html
+documentation.
+
+Tree
+----
+The following directories are present (some only after the
+grunt build):
+ /src - The JavaScript Apache Thrift source
+ /doc - HTML documentation
+ /dist - Distribution files (thrift.js and thrift.min.js)
+ /test - Various tests, this is a good place to look for
+ example code
+ /node_modules - Build support files installed by npm
+
+
+Example JavaScript Client and Server
+------------------------------------
The listing below demonstrates a simple browser based JavaScript
Thrift client and Node.js JavaScript server for the hello_svc
service.
-### hello.thrift - Service IDL
+### hello.thrift - Service IDL
+### build with: $ thrift -gen js -gen js:node hello.thrift
service hello_svc {
string get_message(1: string name)
}
@@ -92,7 +124,8 @@
}
}
- var server = Thrift.createStaticHttpThriftServer(server_opt);
+ var server = Thrift.createThriftWebServer(server_opt);
var port = 9099;
server.listen(port);
- console.log("Http/Thrift Server running on port: " + port);`
+ console.log("Http/Thrift Server running on port: " + port);
+
diff --git a/lib/js/src/thrift.js b/lib/js/src/thrift.js
index d605ab7..411eead 100644
--- a/lib/js/src/thrift.js
+++ b/lib/js/src/thrift.js
@@ -22,8 +22,16 @@
/**
* The Thrift namespace houses the Apache Thrift JavaScript library
* elements providing JavaScript bindings for the Apache Thrift RPC
- * system. Users will typically only directly make use of the
- * Transport and Protocol constructors.
+ * system. End users will typically only directly make use of the
+ * Transport (TXHRTransport/TWebSocketTransport) and Protocol
+ * (TJSONPRotocol/TBinaryProtocol) constructors.
+ *
+ * Object methods beginning with a __ (e.g. __onOpen()) are internal
+ * and should not be called outside of the object's own methods.
+ *
+ * This library creates one global object: Thrift
+ * Code in this library must never create additional global identifiers,
+ * all features must be scoped within the Thrift namespace.
* @namespace
* @example
* var transport = new Thrift.Transport("http://localhost:8585");
@@ -108,7 +116,6 @@
length++;
}
}
-
return length;
},
@@ -266,19 +273,20 @@
};
/**
- * Initializes a Thrift Http[s] transport instance.
- * Note: If you do not specify a url then you must handle XHR on your own
- * (this is how to use js bindings in a async fashion).
+ * Constructor Function for the XHR transport.
+ * If you do not specify a url then you must handle XHR operations on
+ * your own. This type can also be constructed using the Transport alias
+ * for backward compatibility.
* @constructor
* @param {string} [url] - The URL to connect to.
- * @classdesc The Apache Thrift Transport layer performs byte level I/O between RPC
- * clients and servers. The JavaScript Transport object type uses Http[s]/XHR and is
- * the sole browser based Thrift transport. Target servers must implement the http[s]
- * transport (see: node.js example server).
+ * @classdesc The Apache Thrift Transport layer performs byte level I/O
+ * between RPC clients and servers. The JavaScript TXHRTransport object
+ * uses Http[s]/XHR. Target servers must implement the http[s] transport
+ * (see: node.js example server_http.js).
* @example
- * var transport = new Thrift.Transport("http://localhost:8585");
+ * var transport = new Thrift.TXHRTransport("http://localhost:8585");
*/
-Thrift.Transport = function(url) {
+Thrift.Transport = Thrift.TXHRTransport = function(url) {
this.url = url;
this.wpos = 0;
this.rpos = 0;
@@ -287,7 +295,7 @@
this.recv_buf = '';
};
-Thrift.Transport.prototype = {
+Thrift.TXHRTransport.prototype = {
/**
* Gets the browser specific XmlHttpRequest Object.
* @returns {object} the browser XHR interface object
@@ -301,17 +309,17 @@
},
/**
- * Sends the current XRH request if the transport was created with a URL and
- * the async parameter if false. If the transport was not created with a URL
- * or the async parameter is True or the URL is an empty string, the current
- * send buffer is returned.
+ * Sends the current XRH request if the transport was created with a URL
+ * and the async parameter is false. If the transport was not created with
+ * a URL, or the async parameter is True and no callback is provided, or
+ * the URL is an empty string, the current send buffer is returned.
* @param {object} async - If true the current send buffer is returned.
- * @param {object} callback - Optional async callback function, receives the call result.
+ * @param {object} callback - Optional async completion callback
* @returns {undefined|string} Nothing or the current send buffer.
* @throws {string} If XHR fails.
*/
flush: function(async, callback) {
- //async mode
+ var self = this;
if ((async && !callback) || this.url === undefined || this.url === '') {
return this.send_buf;
}
@@ -323,7 +331,18 @@
}
if (callback) {
- xreq.onreadystatechange = callback;
+ //Ignore XHR callbacks until the data arrives, then call the
+ // client's callback
+ xreq.onreadystatechange =
+ (function() {
+ var clientCallback = callback;
+ return function() {
+ if (this.readyState == 4 && this.status == 200) {
+ self.setRecvBuffer(this.responseText);
+ clientCallback();
+ }
+ };
+ }());
}
xreq.open('POST', this.url, !!async);
@@ -350,7 +369,7 @@
* Creates a jQuery XHR object to be used for a Thrift server call.
* @param {object} client - The Thrift Service client object generated by the IDL compiler.
* @param {object} postData - The message to send to the server.
- * @param {function} args - The function to call if the request suceeds.
+ * @param {function} args - The original call arguments with the success call back at the end.
* @param {function} recv_method - The Thrift Service Client receive method for the call.
* @returns {object} A new jQuery XHR object.
* @throws {string} If the jQuery version is prior to 1.5 or if jQuery is not found.
@@ -385,6 +404,180 @@
},
/**
+ * Sets the buffer to provide the protocol when deserializing.
+ * @param {string} buf - The buffer to supply the protocol.
+ */
+ setRecvBuffer: function(buf) {
+ this.recv_buf = buf;
+ this.recv_buf_sz = this.recv_buf.length;
+ this.wpos = this.recv_buf.length;
+ this.rpos = 0;
+ },
+
+ /**
+ * Returns true if the transport is open, XHR always returns true.
+ * @readonly
+ * @returns {boolean} Always True.
+ */
+ isOpen: function() {
+ return true;
+ },
+
+ /**
+ * Opens the transport connection, with XHR this is a nop.
+ */
+ open: function() {},
+
+ /**
+ * Closes the transport connection, with XHR this is a nop.
+ */
+ close: function() {},
+
+ /**
+ * Returns the specified number of characters from the response
+ * buffer.
+ * @param {number} len - The number of characters to return.
+ * @returns {string} Characters sent by the server.
+ */
+ read: function(len) {
+ var avail = this.wpos - this.rpos;
+
+ if (avail === 0) {
+ return '';
+ }
+
+ var give = len;
+
+ if (avail < len) {
+ give = avail;
+ }
+
+ var ret = this.read_buf.substr(this.rpos, give);
+ this.rpos += give;
+
+ //clear buf when complete?
+ return ret;
+ },
+
+ /**
+ * Returns the entire response buffer.
+ * @returns {string} Characters sent by the server.
+ */
+ readAll: function() {
+ return this.recv_buf;
+ },
+
+ /**
+ * Sets the send buffer to buf.
+ * @param {string} buf - The buffer to send.
+ */
+ write: function(buf) {
+ this.send_buf = buf;
+ },
+
+ /**
+ * Returns the send buffer.
+ * @readonly
+ * @returns {string} The send buffer.
+ */
+ getSendBuffer: function() {
+ return this.send_buf;
+ }
+
+};
+
+
+/**
+ * Constructor Function for the WebSocket transport.
+ * @constructor
+ * @param {string} [url] - The URL to connect to.
+ * @classdesc The Apache Thrift Transport layer performs byte level I/O
+ * between RPC clients and servers. The JavaScript TWebSocketTransport object
+ * uses the WebSocket protocol. Target servers must implement WebSocket.
+ * (see: node.js example server_http.js).
+ * @example
+ * var transport = new Thrift.TWebSocketTransport("http://localhost:8585");
+ */
+Thrift.TWebSocketTransport = function(url) {
+ this.__reset(url);
+};
+
+Thrift.TWebSocketTransport.prototype = {
+ __reset: function(url) {
+ this.url = url; //Where to connect
+ this.socket = null; //The web socket
+ this.callbacks = []; //Pending callbacks
+ this.send_pending = []; //Buffers/Callback pairs waiting to be sent
+ this.send_buf = ''; //Outbound data, immutable until sent
+ this.recv_buf = ''; //Inbound data
+ this.rb_wpos = 0; //Network write position in receive buffer
+ this.rb_rpos = 0; //Client read position in receive buffer
+ },
+
+ /**
+ * Sends the current WS request and registers callback. The async
+ * parameter is ignored (WS flush is always async) and the callback
+ * function parameter is required.
+ * @param {object} async - Ignored.
+ * @param {object} callback - The client completion callback.
+ * @returns {undefined|string} Nothing (undefined)
+ */
+ flush: function(async, callback) {
+ var self = this;
+ if (this.isOpen()) {
+ //Send data and register a callback to invoke the client callback
+ this.socket.send(this.send_buf);
+ this.callbacks.push((function() {
+ var clientCallback = callback;
+ return function(msg) {
+ self.setRecvBuffer(msg);
+ clientCallback();
+ };
+ }()));
+ } else {
+ //Queue the send to go out __onOpen
+ this.send_pending.push({
+ buf: this.send_buf,
+ cb: callback
+ });
+ }
+ },
+
+ __onOpen: function() {
+ var self = this;
+ if (this.send_pending.length > 0) {
+ //If the user made calls before the connection was fully
+ //open, send them now
+ this.send_pending.forEach(function(elem) {
+ this.socket.send(elem.buf);
+ this.callbacks.push((function() {
+ var clientCallback = elem.cb;
+ return function(msg) {
+ self.setRecvBuffer(msg);
+ clientCallback();
+ };
+ }()));
+ });
+ this.send_pending = [];
+ }
+ },
+
+ __onClose: function(evt) {
+ this.__reset(this.url);
+ },
+
+ __onMessage: function(evt) {
+ if (this.callbacks.length) {
+ this.callbacks.shift()(evt.data);
+ }
+ },
+
+ __onError: function(evt) {
+ console.log("Thrift WebSocket Error: " + evt.toString());
+ this.socket.close();
+ },
+
+ /**
* Sets the buffer to use when receiving server responses.
* @param {string} buf - The buffer to receive server responses.
*/
@@ -396,26 +589,36 @@
},
/**
- * Returns true if the transport is open, in browser based JavaScript
- * this function always returns true.
+ * Returns true if the transport is open
* @readonly
- * @returns {boolean} Always True.
+ * @returns {boolean}
*/
isOpen: function() {
- return true;
+ return this.socket && this.socket.readyState == this.socket.OPEN;
},
/**
- * Opens the transport connection, in browser based JavaScript
- * this function is a nop.
+ * Opens the transport connection
*/
- open: function() {},
+ open: function() {
+ //If OPEN/CONNECTING/CLOSING ignore additional opens
+ if (this.socket && this.socket.readyState != this.socket.CLOSED) {
+ return;
+ }
+ //If there is no socket or the socket is closed:
+ this.socket = new WebSocket(this.url);
+ this.socket.onopen = this.__onOpen.bind(this);
+ this.socket.onmessage = this.__onMessage.bind(this);
+ this.socket.onerror = this.__onError.bind(this);
+ this.socket.onclose = this.__onClose.bind(this);
+ },
/**
- * Closes the transport connection, in browser based JavaScript
- * this function is a nop.
+ * Closes the transport connection
*/
- close: function() {},
+ close: function() {
+ this.socket.close();
+ },
/**
* Returns the specified number of characters from the response
@@ -1161,3 +1364,4 @@
};
+
diff --git a/lib/js/test/README b/lib/js/test/README
new file mode 100644
index 0000000..6923794
--- /dev/null
+++ b/lib/js/test/README
@@ -0,0 +1,63 @@
+Thrift Javascript Library
+=========================
+This browser based Apache Thrift implementation supports
+RPC clients using the JSON protocol over Http[s] with XHR.
+
+License
+-------
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+
+Test Servers
+------------
+drwxr-xr-x 2 randy randy 4096 Feb 8 15:44 sec
+-rw-r--r-- 1 randy randy 2183 Feb 9 04:01 server_http.js
+-rw-r--r-- 1 randy randy 2386 Feb 9 05:39 server_https.js
+
+server_http.js is a Node.js web server which support the
+standard Apache Thrift test suite (thrift/test/ThriftTest.thrift).
+The server supports Apache Thrift XHR and WebSocket clients.
+
+server_https.js is the same but uses SSL/TLS. The sec directory
+contains the server key and certificate used by the ssl server.
+Both of these servers support WebSocket (the http: supports ws:,
+and the https: support wss:).
+
+To run the test servers use: $ make check (requires
+the Apache Thrift Java branch and make check must have
+been run in thrift/lib/java previously) or run the grunt
+ build in the parent js directory (see README there).
+
+Test Clients
+------------
+-rw-r--r-- 1 randy randy 13558 Feb 9 07:18 test-async.js
+-rw-r--r-- 1 randy randy 5724 Feb 9 03:45 test_handler.js
+-rwxr-xr-x 1 randy randy 2719 Feb 9 06:04 test.html
+-rw-r--r-- 1 randy randy 4611 Feb 9 06:05 test-jq.js
+-rwxr-xr-x 1 randy randy 12153 Feb 9 06:04 test.js
+-rw-r--r-- 1 randy randy 2593 Feb 9 06:16 test-nojq.html
+-rw-r--r-- 1 randy randy 1450 Feb 9 06:14 test-nojq.js
+-rw-r--r-- 1 randy randy 2847 Feb 9 06:31 testws.html
+
+There are three html test driver files, all of which are
+QUnit based. test.html test the Apache Thrift jQuery
+generated code (thrift -gen js:jquery). The test-nojq.html
+Runs almost identical tests against normal JavaScript builds
+(thrift -gen js). Both of the previous tests use the XHR
+transport. The testws.html runs similar tests using the
+WebSocket transport. The test*.js files are loaded by the
+html drivers and contain the actualApache Thrift tests.
diff --git a/lib/js/test/server_http.js b/lib/js/test/server_http.js
index 623a979..01174bc 100644
--- a/lib/js/test/server_http.js
+++ b/lib/js/test/server_http.js
@@ -39,14 +39,14 @@
handler: ThriftTestHandler
};
-var StaticHttpThriftServerOptions = {
+var ThriftWebServerOptions = {
staticFilePath: ".",
services: {
"/service": ThriftTestSvcOpt
}
};
-var server = thrift.createStaticHttpThriftServer(StaticHttpThriftServerOptions);
+var server = thrift.createWebServer(ThriftWebServerOptions);
var port = 8088;
server.listen(port);
console.log("Serving files from: " + __dirname);
diff --git a/lib/js/test/server_https.js b/lib/js/test/server_https.js
new file mode 100644
index 0000000..28f0585
--- /dev/null
+++ b/lib/js/test/server_https.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//This HTTP server is designed to server the test.html browser
+// based JavaScript test page (which must be in the current directory).
+// This server also supplies the Thrift based test service, which depends
+// on the standard ThriftTest.thrift IDL service (which must be compiled
+// for Node and browser based JavaScript in ./gen-nodejs and ./gen-js
+// respectively). The current directory must also include the browser
+// support libraries for test.html (jquery.js, qunit.js and qunit.css
+// in ./build/js/lib).
+
+var fs = require("fs");
+var thrift = require('../../nodejs/lib/thrift');
+var TBufferedTransport = require('../../nodejs/lib/thrift/transport').TBufferedTransport;
+var TJSONProtocol = require('../../nodejs/lib/thrift/protocol').TJSONProtocol;
+var ThriftTestSvc = require('./gen-nodejs/ThriftTest.js');
+var ThriftTestHandler = require('./test_handler').ThriftTestHandler;
+
+//Setup the I/O stack options for the ThriftTest service
+var ThriftTestSvcOpt = {
+ transport: TBufferedTransport,
+ protocol: TJSONProtocol,
+ cls: ThriftTestSvc,
+ handler: ThriftTestHandler
+};
+
+var ThriftWebServerOptions = {
+ staticFilePath: ".",
+ tlsOptions: {
+ key: fs.readFileSync("../../../test/keys/server.key"),
+ cert: fs.readFileSync("../../../test/keys/server.crt")
+ },
+ services: {
+ "/service": ThriftTestSvcOpt
+ }
+};
+
+var server = thrift.createWebServer(ThriftWebServerOptions);
+var port = 8089;
+server.listen(port);
+console.log("Serving files from: " + __dirname);
+console.log("Http/Thrift Server running on port: " + port);
+
+
+
diff --git a/lib/js/test/test-async.js b/lib/js/test/test-async.js
new file mode 100644
index 0000000..4935fea
--- /dev/null
+++ b/lib/js/test/test-async.js
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+ /* jshint -W100 */
+
+/*
+ * Fully Async JavaScript test suite for ThriftTest.thrift.
+ * These tests are designed to exercise the WebSocket transport
+ * (which is exclusively async).
+ *
+ * To compile client code for this test use:
+ * $ thrift -gen js ThriftTest.thrift
+ */
+
+
+
+// all Languages in UTF-8
+var stringTest = "Afrikaans, Alemannisch, Aragonés, العربية, مصرى, Asturianu, Aymar aru, Azərbaycan, Башҡорт, Boarisch, Žemaitėška, Беларуская, Беларуская (тарашкевіца), Български, Bamanankan, বাংলা, Brezhoneg, Bosanski, Català, Mìng-dĕ̤ng-ngṳ̄, Нохчийн, Cebuano, ᏣᎳᎩ, Česky, Словѣ́ньскъ / ⰔⰎⰑⰂⰡⰐⰠⰔⰍⰟ, Чӑвашла, Cymraeg, Dansk, Zazaki, ދިވެހިބަސް, Ελληνικά, Emiliàn e rumagnòl, English, Esperanto, Español, Eesti, Euskara, فارسی, Suomi, Võro, Føroyskt, Français, Arpetan, Furlan, Frysk, Gaeilge, 贛語, Gàidhlig, Galego, Avañe'ẽ, ગુજરાતી, Gaelg, עברית, हिन्दी, Fiji Hindi, Hrvatski, Kreyòl ayisyen, Magyar, Հայերեն, Interlingua, Bahasa Indonesia, Ilokano, Ido, Íslenska, Italiano, 日本語, Lojban, Basa Jawa, ქართული, Kongo, Kalaallisut, ಕನ್ನಡ, 한국어, Къарачай-Малкъар, Ripoarisch, Kurdî, Коми, Kernewek, Кыргызча, Latina, Ladino, Lëtzebuergesch, Limburgs, Lingála, ລາວ, Lietuvių, Latviešu, Basa Banyumasan, Malagasy, Македонски, മലയാളം, मराठी, Bahasa Melayu, مازِرونی, Nnapulitano, Nedersaksisch, नेपाल भाषा, Nederlands, Norsk (nynorsk), Norsk (bokmål), Nouormand, Diné bizaad, Occitan, Иронау, Papiamentu, Deitsch, Norfuk / Pitkern, Polski, پنجابی, پښتو, Português, Runa Simi, Rumantsch, Romani, Română, Русский, Саха тыла, Sardu, Sicilianu, Scots, Sámegiella, Simple English, Slovenčina, Slovenščina, Српски / Srpski, Seeltersk, Svenska, Kiswahili, தமிழ், తెలుగు, Тоҷикӣ, ไทย, Türkmençe, Tagalog, Türkçe, Татарча/Tatarça, Українська, اردو, Tiếng Việt, Volapük, Walon, Winaray, 吴语, isiXhosa, ייִדיש, Yorùbá, Zeêuws, 中文, Bân-lâm-gú, 粵語";
+
+function checkRecursively(map1, map2) {
+ if (typeof map1 !== 'function' && typeof map2 !== 'function') {
+ if (!map1 || typeof map1 !== 'object') {
+ equal(map1, map2);
+ } else {
+ for (var key in map1) {
+ checkRecursively(map1[key], map2[key]);
+ }
+ }
+ }
+}
+
+module("Base Types");
+
+ asyncTest("Void", function() {
+ expect( 1 );
+ client.testVoid(function(result) {
+ equal(result, undefined);
+ QUnit.start();
+ });
+ });
+
+
+ asyncTest("String", function() {
+ expect( 3 );
+ QUnit.stop(2);
+ client.testString('', function(result){
+ equal(result, '');
+ QUnit.start();
+ });
+ client.testString(stringTest, function(result){
+ equal(result, stringTest);
+ QUnit.start();
+ });
+
+ var specialCharacters = 'quote: \" backslash:' +
+ ' forwardslash-escaped: \/ ' +
+ ' backspace: \b formfeed: \f newline: \n return: \r tab: ' +
+ ' now-all-of-them-together: "\\\/\b\n\r\t' +
+ ' now-a-bunch-of-junk: !@#$%&()(&%$#{}{}<><><';
+ client.testString(specialCharacters, function(result){
+ equal(result, specialCharacters);
+ QUnit.start();
+ });
+ });
+ asyncTest("Double", function() {
+ expect( 4 );
+ QUnit.stop(3);
+ client.testDouble(0, function(result){
+ equal(result, 0);
+ QUnit.start();
+ });
+ client.testDouble(-1, function(result){
+ equal(result, -1);
+ QUnit.start();
+ });
+ client.testDouble(3.14, function(result){
+ equal(result, 3.14);
+ QUnit.start();
+ });
+ client.testDouble(Math.pow(2,60), function(result){
+ equal(result, Math.pow(2,60));
+ QUnit.start();
+ });
+ });
+ asyncTest("Byte", function() {
+ expect( 2 );
+ QUnit.stop();
+ client.testByte(0, function(result) {
+ equal(result, 0);
+ QUnit.start();
+ });
+ client.testByte(0x01, function(result) {
+ equal(result, 0x01);
+ QUnit.start();
+ });
+ });
+ asyncTest("I32", function() {
+ expect( 3 );
+ QUnit.stop(2);
+ client.testI32(0, function(result){
+ equal(result, 0);
+ QUnit.start();
+ });
+ client.testI32(Math.pow(2,30), function(result){
+ equal(result, Math.pow(2,30));
+ QUnit.start();
+ });
+ client.testI32(-Math.pow(2,30), function(result){
+ equal(result, -Math.pow(2,30));
+ QUnit.start();
+ });
+ });
+ asyncTest("I64", function() {
+ expect( 3 );
+ QUnit.stop(2);
+ client.testI64(0, function(result){
+ equal(result, 0);
+ QUnit.start();
+ });
+ //This is usually 2^60 but JS cannot represent anything over 2^52 accurately
+ client.testI64(Math.pow(2,52), function(result){
+ equal(result, Math.pow(2,52));
+ QUnit.start();
+ });
+ client.testI64(-Math.pow(2,52), function(result){
+ equal(result, -Math.pow(2,52));
+ QUnit.start();
+ });
+ });
+
+
+
+
+module("Structured Types");
+
+ asyncTest("Struct", function() {
+ expect( 5 );
+ var structTestInput = new ThriftTest.Xtruct();
+ structTestInput.string_thing = 'worked';
+ structTestInput.byte_thing = 0x01;
+ structTestInput.i32_thing = Math.pow(2,30);
+ //This is usually 2^60 but JS cannot represent anything over 2^52 accurately
+ structTestInput.i64_thing = Math.pow(2,52);
+
+ client.testStruct(structTestInput, function(result){
+ equal(result.string_thing, structTestInput.string_thing);
+ equal(result.byte_thing, structTestInput.byte_thing);
+ equal(result.i32_thing, structTestInput.i32_thing);
+ equal(result.i64_thing, structTestInput.i64_thing);
+ equal(JSON.stringify(result), JSON.stringify(structTestInput));
+ QUnit.start();
+ });
+ });
+
+ asyncTest("Nest", function() {
+ expect( 7 );
+ var xtrTestInput = new ThriftTest.Xtruct();
+ xtrTestInput.string_thing = 'worked';
+ xtrTestInput.byte_thing = 0x01;
+ xtrTestInput.i32_thing = Math.pow(2,30);
+ //This is usually 2^60 but JS cannot represent anything over 2^52 accurately
+ xtrTestInput.i64_thing = Math.pow(2,52);
+
+ var nestTestInput = new ThriftTest.Xtruct2();
+ nestTestInput.byte_thing = 0x02;
+ nestTestInput.struct_thing = xtrTestInput;
+ nestTestInput.i32_thing = Math.pow(2,15);
+
+ client.testNest(nestTestInput, function(result){
+ equal(result.byte_thing, nestTestInput.byte_thing);
+ equal(result.struct_thing.string_thing, nestTestInput.struct_thing.string_thing);
+ equal(result.struct_thing.byte_thing, nestTestInput.struct_thing.byte_thing);
+ equal(result.struct_thing.i32_thing, nestTestInput.struct_thing.i32_thing);
+ equal(result.struct_thing.i64_thing, nestTestInput.struct_thing.i64_thing);
+ equal(result.i32_thing, nestTestInput.i32_thing);
+ equal(JSON.stringify(result), JSON.stringify(nestTestInput));
+ QUnit.start();
+ });
+ });
+
+ asyncTest("Map", function() {
+ expect( 3 );
+ var mapTestInput = {7:77, 8:88, 9:99};
+
+ client.testMap(mapTestInput, function(result){
+ for (var key in result) {
+ equal(result[key], mapTestInput[key]);
+ }
+ QUnit.start();
+ });
+ });
+
+ asyncTest("StringMap", function() {
+ expect( 6 );
+ var mapTestInput = {
+ "a":"123", "a b":"with spaces ", "same":"same", "0":"numeric key",
+ "longValue":stringTest, stringTest:"long key"
+ };
+
+ client.testStringMap(mapTestInput, function(result){
+ for (var key in result) {
+ equal(result[key], mapTestInput[key]);
+ }
+ QUnit.start();
+ });
+ });
+
+ asyncTest("Set", function() {
+ expect( 1 );
+ var setTestInput = [1,2,3];
+ client.testSet(setTestInput, function(result){
+ ok(result, setTestInput);
+ QUnit.start();
+ });
+ });
+
+ asyncTest("List", function() {
+ expect( 1 );
+ var listTestInput = [1,2,3];
+ client.testList(listTestInput, function(result){
+ ok(result, listTestInput);
+ QUnit.start();
+ });
+ });
+
+ asyncTest("Enum", function() {
+ expect( 1 );
+ client.testEnum(ThriftTest.Numberz.ONE, function(result){
+ equal(result, ThriftTest.Numberz.ONE);
+ QUnit.start();
+ });
+ });
+
+ asyncTest("TypeDef", function() {
+ expect( 1 );
+ client.testTypedef(69, function(result){
+ equal(result, 69);
+ QUnit.start();
+ });
+ });
+
+
+module("deeper!");
+
+ asyncTest("MapMap", function() {
+ expect( 16 );
+ var mapMapTestExpectedResult = {
+ "4":{"1":1,"2":2,"3":3,"4":4},
+ "-4":{"-4":-4, "-3":-3, "-2":-2, "-1":-1}
+ };
+
+ client.testMapMap(1, function(result){
+ for (var key in result) {
+ for (var key2 in result[key]) {
+ equal(result[key][key2], mapMapTestExpectedResult[key][key2]);
+ }
+ }
+ checkRecursively(result, mapMapTestExpectedResult);
+ QUnit.start();
+ });
+ });
+
+
+module("Exception");
+
+ asyncTest("Xception", function() {
+ expect(2);
+ client.testException("Xception", function(e){
+ equal(e.errorCode, 1001);
+ equal(e.message, "Xception");
+ QUnit.start();
+ });
+ });
+
+ asyncTest("no Exception", 0, function() {
+ expect( 1 );
+ client.testException("no Exception", function(e){
+ ok(!e);
+ QUnit.start();
+ });
+ });
+
+module("Insanity");
+
+ asyncTest("testInsanity", function() {
+ expect( 24 );
+ var insanity = {
+ "1":{
+ "2":{
+ "userMap":{ "5":5, "8":8 },
+ "xtructs":[{
+ "string_thing":"Goodbye4",
+ "byte_thing":4,
+ "i32_thing":4,
+ "i64_thing":4
+ },
+ {
+ "string_thing":"Hello2",
+ "byte_thing":2,
+ "i32_thing":2,
+ "i64_thing":2
+ }
+ ]
+ },
+ "3":{
+ "userMap":{ "5":5, "8":8 },
+ "xtructs":[{
+ "string_thing":"Goodbye4",
+ "byte_thing":4,
+ "i32_thing":4,
+ "i64_thing":4
+ },
+ {
+ "string_thing":"Hello2",
+ "byte_thing":2,
+ "i32_thing":2,
+ "i64_thing":2
+ }
+ ]
+ }
+ },
+ "2":{ "6":{ "userMap":null, "xtructs":null } }
+ };
+ client.testInsanity(new ThriftTest.Insanity(), function(res){
+ ok(res, JSON.stringify(res));
+ ok(insanity, JSON.stringify(insanity));
+ checkRecursively(res, insanity);
+ QUnit.start();
+ });
+ });
+
+
diff --git a/lib/js/test/test-jq.js b/lib/js/test/test-jq.js
index ed658e4..64608fe 100644
--- a/lib/js/test/test-jq.js
+++ b/lib/js/test/test-jq.js
@@ -29,9 +29,6 @@
* ++ test-nojq.js for "-gen js" only tests
*/
-var transport = new Thrift.Transport("/service");
-var protocol = new Thrift.Protocol(transport);
-var client = new ThriftTest.ThriftTestClient(protocol);
//////////////////////////////////
//jQuery asynchronous tests
diff --git a/lib/js/test/test-nojq.js b/lib/js/test/test-nojq.js
index f67ea62..19f9e61 100644
--- a/lib/js/test/test-nojq.js
+++ b/lib/js/test/test-nojq.js
@@ -29,9 +29,6 @@
* ++ test-jq.js for "-gen js:jquery" only tests
*/
-var transport = new Thrift.Transport("/service");
-var protocol = new Thrift.Protocol(transport);
-var client = new ThriftTest.ThriftTestClient(protocol);
//////////////////////////////////
//Async exception tests
diff --git a/lib/js/test/test.html b/lib/js/test/test.html
index 8f9e7ee..91d1a97 100755
--- a/lib/js/test/test.html
+++ b/lib/js/test/test.html
@@ -27,13 +27,18 @@
<script src="gen-js/ThriftTest.js" type="text/javascript" charset="utf-8"></script>
<!-- jQuery -->
- <script type="text/javascript" src="build/js/lib/jquery-1.7.2.js" charset="utf-8"></script>
+ <script type="text/javascript" src="http://code.jquery.com/jquery-1.7.2.js" charset="utf-8"></script>
<!-- QUnit Test framework-->
- <script type="text/javascript" src="build/js/lib/qunit.js" charset="utf-8"></script>
- <link rel="stylesheet" href="build/js/lib/qunit.css" type="text/css" media="screen" />
+ <script type="text/javascript" src="http://code.jquery.com/qunit/qunit-1.14.0.js" charset="utf-8"></script>
+ <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.14.0.css" type="text/css" media="screen" />
<!-- the Test Suite-->
+ <script>
+ var transport = new Thrift.Transport("/service");
+ var protocol = new Thrift.Protocol(transport);
+ var client = new ThriftTest.ThriftTestClient(protocol);
+ </script>
<script type="text/javascript" src="test.js" charset="utf-8"></script>
<script type="text/javascript" src="test-jq.js" charset="utf-8"></script>
</head>
diff --git a/lib/js/test/test_handler.js b/lib/js/test/test_handler.js
index 33c8941..17d22cf 100644
--- a/lib/js/test/test_handler.js
+++ b/lib/js/test/test_handler.js
@@ -21,7 +21,7 @@
// Apache Thrift test service.
var ttypes = require('./gen-nodejs/ThriftTest_types');
-var TException = require('thrift/thrift').TException;
+var TException = require('../../nodejs/lib/thrift').TException;
var ThriftTestHandler = exports.ThriftTestHandler = {
testVoid: function(result) {
diff --git a/lib/js/test/testws.html b/lib/js/test/testws.html
new file mode 100644
index 0000000..15ee195
--- /dev/null
+++ b/lib/js/test/testws.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>Thrift Javascript Bindings: Unit Test</title>
+
+ <script src="build/js/thrift.js" type="text/javascript" charset="utf-8"></script>
+ <script src="gen-js/ThriftTest_types.js" type="text/javascript" charset="utf-8"></script>
+ <script src="gen-js/ThriftTest.js" type="text/javascript" charset="utf-8"></script>
+
+ <!-- jQuery -->
+ <script type="text/javascript" src="https://code.jquery.com/jquery-1.7.2.js" charset="utf-8"></script>
+
+ <!-- QUnit Test framework-->
+ <script type="text/javascript" src="https://code.jquery.com/qunit/qunit-1.14.0.js" charset="utf-8"></script>
+ <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.14.0.css" type="text/css" media="screen" />
+
+ <!-- the Test Suite-->
+ <script>
+ var loc = window.location;
+ var ws_uri = ((loc.protocol === "https:") ? "wss://" : "ws://") +
+ loc.hostname + ":" + loc.port + loc.pathname;
+ var transport = new Thrift.TWebSocketTransport(ws_uri);
+ var protocol = new Thrift.Protocol(transport);
+ var client = new ThriftTest.ThriftTestClient(protocol);
+ transport.open();
+ </script>
+ <script type="text/javascript" src="test-async.js" charset="utf-8"></script>
+</head>
+<body>
+ <h1 id="qunit-header">Thrift Javascript Bindings: Unit Test (<a href="https://git-wip-us.apache.org/repos/asf?p=thrift.git;a=blob;f=test/ThriftTest.thrift;hb=HEAD">ThriftTest.thrift</a>)</h1>
+ <h2 id="qunit-banner"></h2>
+ <div id="qunit-testrunner-toolbar"></div>
+ <h2 id="qunit-userAgent"></h2>
+ <ol id="qunit-tests"><li><!-- get valid xhtml strict--></li></ol>
+ <p>
+ <a href="http://validator.w3.org/check/referer"><img
+ src="http://www.w3.org/Icons/valid-xhtml10"
+ alt="Valid XHTML 1.0!" height="31" width="88" /></a>
+ </p>
+</body>
+</html>
diff --git a/lib/nodejs/lib/thrift/index.js b/lib/nodejs/lib/thrift/index.js
index 8499f9a..8487464 100644
--- a/lib/nodejs/lib/thrift/index.js
+++ b/lib/nodejs/lib/thrift/index.js
@@ -35,8 +35,8 @@
exports.createMultiplexServer = server.createMultiplexServer;
exports.createMultiplexSSLServer = server.createMultiplexSSLServer;
-var static_server = require('./static_server');
-exports.createStaticHttpThriftServer = static_server.createStaticHttpThriftServer;
+var web_server = require('./web_server');
+exports.createWebServer = web_server.createWebServer;
exports.Int64 = require('node-int64');
exports.Q = require('q');
diff --git a/lib/nodejs/lib/thrift/static_server.js b/lib/nodejs/lib/thrift/static_server.js
deleted file mode 100644
index b61bd30..0000000
--- a/lib/nodejs/lib/thrift/static_server.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-var http = require('http');
-var url = require("url");
-var path = require("path");
-var fs = require("fs");
-
-var ttransport = require('./transport');
-var TBinaryProtocol = require('./protocol').TBinaryProtocol;
-
-/**
- * @class
- * @name StaticHttpThriftServerOptions
- * @property {string} staticFilePath - Path to serve static files from, default
- * is ".", use "" to disable static file service.
- * @property {object} services - An object hash mapping service URIs to
- * ThriftServiceOptions objects.
- * @see {@link ThriftServiceOptions}
- */
-
-/**
- * @class
- * @name ThriftServiceOptions
- * @property {object} transport - The layered transport to use (defaults to none).
- * @property {object} protocol - The Thrift Protocol to use (defaults to TBinaryProtocol).
- * @property {object} cls - The Thrift Service class generated by the IDL Compiler for the service.
- * @property {object} handler - The handler methods for the Thrift Service.
- */
-
-/**
- * Creates a Thrift server which can serve static files and/or one or
- * more Thrift Services.
- * @param {StaticHttpThriftServerOptions} - The server configuration.
- * @returns {object} - The Thrift server.
- */
-exports.createStaticHttpThriftServer = function(options) {
- //Set the static dir to serve files from
- var baseDir = typeof options.staticFilePath != "string" ? process.cwd() : options.staticFilePath;
- var contentTypesByExtension = {
- '.html': "text/html",
- '.css': "text/css",
- '.js': "text/javascript"
- };
-
- //Setup all of the services
- var services = options.services;
- for (svc in services) {
- var svcObj = services[svc];
- var processor = svcObj.cls.Processor || svcObj.cls;
- svcObj.processor = new processor(svcObj.handler);
- svcObj.transport = svcObj.transport ? svcObj.transport : ttransport.TBufferedTransport;
- svcObj.protocol = svcObj.protocol ? svcObj.protocol : TBinaryProtocol;
- }
-
- //Handle POST methods
- function processPost(request, response) {
- var uri = url.parse(request.url).pathname;
- var svc = services[uri];
- if (!svc) {
- //Todo: add support for non Thrift posts
- response.writeHead(500);
- response.end();
- return;
- }
-
- request.on('data', svc.transport.receiver(function(transportWithData) {
- var input = new svc.protocol(transportWithData);
- var output = new svc.protocol(new svc.transport(undefined, function(buf) {
- try {
- response.writeHead(200);
- response.end(buf);
- } catch (err) {
- response.writeHead(500);
- response.end();
- }
- }));
-
- try {
- svc.processor.process(input, output);
- transportWithData.commitPosition();
- }
- catch (err) {
- if (err instanceof ttransport.InputBufferUnderrunError) {
- transportWithData.rollbackPosition();
- }
- else {
- response.writeHead(500);
- response.end();
- }
- }
- }));
- }
-
- //Handle GET methods
- function processGet(request, response) {
- //An empty string base directory means do not serve static files
- if ("" == baseDir) {
- response.writeHead(404);
- response.end();
- return;
- }
- //Locate the file requested
- var uri = url.parse(request.url).pathname;
- var filename = path.join(baseDir, uri);
- fs.exists(filename, function(exists) {
- if(!exists) {
- response.writeHead(404);
- response.end();
- return;
- }
-
- if (fs.statSync(filename).isDirectory()) {
- filename += '/index.html';
- }
-
- fs.readFile(filename, "binary", function(err, file) {
- if (err) {
- response.writeHead(500);
- response.end(err + "\n");
- return;
- }
- var headers = {};
- var contentType = contentTypesByExtension[path.extname(filename)];
- if (contentType) {
- headers["Content-Type"] = contentType;
- }
- response.writeHead(200, headers);
- response.write(file, "binary");
- response.end();
- });
- });
- }
-
- //Return the server
- return http.createServer(function(request, response) {
- if (request.method === 'POST') {
- processPost(request, response);
- } else if (request.method === 'GET') {
- processGet(request, response);
- } else {
- response.writeHead(500);
- response.end();
- }
- });
-};
-
diff --git a/lib/nodejs/lib/thrift/web_server.js b/lib/nodejs/lib/thrift/web_server.js
new file mode 100644
index 0000000..c888a80
--- /dev/null
+++ b/lib/nodejs/lib/thrift/web_server.js
@@ -0,0 +1,427 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+var http = require('http');
+var https = require('https');
+var url = require("url");
+var path = require("path");
+var fs = require("fs");
+var crypto = require("crypto");
+
+var TTransport = require('./transport');
+var TBufferedTransport = require('./transport').TBufferedTransport;
+var TBinaryProtocol = require('./protocol').TBinaryProtocol;
+
+
+// WSFrame constructor and prototype
+/////////////////////////////////////////////////////////////////////
+
+/** Apache Thrift RPC Web Socket Frame Layout
+ * Conforming to RFC 6455 circa 12/2011
+ *
+ * Theoretical frame size limit is 4GB*4GB, however the Node Buffer
+ * limit is 1GB as of v0.10. The frame length encoding is also
+ * configured for a max of 4GB presently and needs to be adjusted
+ * if Node/Browsers become capabile of > 4GB frames.
+ *
+ * data - buffer to send (data.length is length to transmit)
+ * mask - Must be null if sending to client or mask-key if sending to server
+ * binEncoding - true for binary, false for text (the default)
+ *
+ * - FIN is always 1, ATRPC messages are sent in a single frame
+ * - RSV1/2/3 are always 0
+ * - Opcode is 1(TEXT) for TJSONProtocol and 2(BIN) for TBinaryProtocol
+ * - Mask Present bit is 1 sending to-server and 0 sending to-client
+ * - Payload Len:
+ * + If < 126: then represented directly
+ * + If >=126: but within range of an unsigned 16 bit integer
+ * then Payload Len is 126 and the two following bytes store
+ * the length
+ * + Else: Payload Len is 127 and the following 8 bytes store the
+ * length as an unsigned 64 bit integer
+ * - Masking key is a 32 bit key only present when sending to the server
+ * - Payload follows the masking key or length
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-------+-+-------------+-------------------------------+
+ * |F|R|R|R| opcode|M| Payload len | Extended payload length |
+ * |I|S|S|S| (4) |A| (7) | (16/64) |
+ * |N|V|V|V| |S| | (if payload len==126/127) |
+ * | |1|2|3| |K| | |
+ * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+ * | Extended payload length continued, if payload len == 127 |
+ * + - - - - - - - - - - - - - - - +-------------------------------+
+ * | |Masking-key, if MASK set to 1 |
+ * +-------------------------------+-------------------------------+
+ * | Masking-key (continued) | Payload Data |
+ * +-------------------------------- - - - - - - - - - - - - - - - +
+ * : Payload Data continued ... :
+ * + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+ * | Payload Data continued ... |
+ * +---------------------------------------------------------------+
+ */
+var wsFrame = {
+ /** Encodes a WebSocket frame
+ *
+ * @param {Buffer} data - The raw data to encode
+ * @param {Buffer} mask - The mask to apply when sending to server, null for no mask
+ * @param {Boolean} binEncoding - True for binary encoding, false for text encoding
+ * @returns {Buffer} - The WebSocket frame, ready to send
+ */
+ encode: function(data, mask, binEncoding) {
+ var frame = new Buffer(wsFrame.frameSizeFromData(data, mask));
+ //Byte 0 - FIN & OPCODE
+ frame[0] = wsFrame.fin.FIN +
+ (binEncoding ? wsFrame.frameOpCodes.BIN : wsFrame.frameOpCodes.TEXT);
+ //Byte 1 or 1-3 or 1-9 - MASK FLAG & SIZE
+ var payloadOffset = 2;
+ if (data.length < 0x7E) {
+ frame[1] = data.length + (mask ? wsFrame.mask.TO_SERVER : wsFrame.mask.TO_CLIENT);
+ } else if (data.length < 0xFFFF) {
+ frame[1] = 0x7E + (mask ? wsFrame.mask.TO_SERVER : wsFrame.mask.TO_CLIENT);
+ frame.writeUInt16BE(data.length, 2, true);
+ payloadOffset = 4;
+ } else {
+ frame[1] = 0x7F + (mask ? wsFrame.mask.TO_SERVER : wsFrame.mask.TO_CLIENT);
+ frame.writeUInt32BE(0, 2, true);
+ frame.writeUInt32BE(data.length, 6, true);
+ payloadOffset = 10;
+ }
+ //MASK
+ if (mask) {
+ mask.copy(frame, payloadOffset, 0, 4);
+ payloadOffset += 4;
+ }
+ //Payload
+ data.copy(frame, payloadOffset);
+ if (mask) {
+ wsFrame.applyMask(frame.slice(payloadOffset), frame.slice(payloadOffset-4,payloadOffset));
+ }
+ return frame;
+ },
+
+ /** Decodes a WebSocket frame
+ *
+ * @param {Buffer} frame - The raw inbound frame
+ * @returns {WSDecodeResult} - The decoded payload
+ */
+ decode: function(frame) {
+ var result = {
+ data: null,
+ mask: null,
+ binEncoding: false,
+ nextFrame: null
+ };
+ //Byte 0 - FIN & OPCODE
+ if (wsFrame.fin.FIN != (frame[0] & wsFrame.fin.FIN)) {
+ console.log("WebSocket frame error: Received a frame without fin set.");
+ }
+ result.binEncoding = (wsFrame.frameOpCodes.BIN == (frame[0] & wsFrame.frameOpCodes.BIN));
+ //Byte 1 or 1-3 or 1-9 - SIZE
+ var lenByte = (frame[1] & 0x0000007F);
+ var len = lenByte;
+ var dataOffset = 2;
+ if (lenByte == 0x7E) {
+ len = frame.readUInt16BE(2);
+ dataOffset = 4;
+ } else if (lenByte == 0x7F) {
+ len = frame.readUInt32BE(6);
+ dataOffset = 10;
+ }
+ //MASK
+ if (wsFrame.mask.TO_SERVER == (frame[1] & wsFrame.mask.TO_SERVER)) {
+ result.mask = new Buffer(4);
+ frame.copy(result.mask, 0, dataOffset, dataOffset + 4);
+ dataOffset += 4;
+ }
+ //Payload
+ result.data = new Buffer(len);
+ frame.copy(result.data, 0, dataOffset, dataOffset+len);
+ wsFrame.applyMask(result.data, result.mask);
+
+ //Residual Frames
+ if (frame.length > dataOffset+len) {
+ result.nextFrame = new Buffer(frame.length - (dataOffset+len));
+ frame.copy(result.nextFrame, 0, dataOffset+len, frame.length);
+ }
+
+ return result;
+ },
+
+ /** Masks/Unmasks data
+ *
+ * @param {Buffer} data - data to mask/unmask in place
+ * @param {Buffer} mask - the mask
+ */
+ applyMask: function(data, mask){
+ //TODO: look into xoring words at a time
+ var dataLen = data.length;
+ var maskLen = mask.length;
+ for (var i = 0; i < dataLen; i++) {
+ data[i] = data[i] ^ mask[i%maskLen];
+ }
+ },
+
+ /** Computes frame size on the wire from data to be sent
+ *
+ * @param {Buffer} data - data.length is the assumed payload size
+ * @param {Boolean} mask - true if a mask will be sent (TO_SERVER)
+ */
+ frameSizeFromData: function(data, mask) {
+ var headerSize = 10;
+ if (data.length < 0x7E) {
+ headerSize = 2;
+ } else if (data.length < 0xFFFF) {
+ headerSize = 4;
+ }
+ return headerSize + data.length + (mask ? 4 : 0);
+ },
+
+ frameOpCodes: {
+ CONT: 0x00,
+ TEXT: 0x01,
+ BIN: 0x02
+ },
+
+ mask: {
+ TO_SERVER: 0x80,
+ TO_CLIENT: 0x00
+ },
+
+ fin: {
+ CONT: 0x00,
+ FIN: 0x80
+ }
+};
+
+
+// createWebServer constructor and options
+/////////////////////////////////////////////////////////////////////
+
+/**
+ * @class
+ * @name ThriftWebServerOptions
+ * @property {string} staticFilePath - Path to serve static files from, if
+ * absent or "" static file service is disabled
+ * @property {TLSOptions} tlsOptions - Node.js TLS options
+ * (see: nodejs.org/api/tls.html), if not present or null regular http
+ * is used, at least a key and a cert must be defined to use SSL/TLS
+ * @property {object} services - An object hash mapping service URIs to
+ * ThriftServiceOptions objects
+ * @see {@link ThriftServiceOptions}
+ */
+
+/**
+ * @class
+ * @name ThriftServiceOptions
+ * @property {object} transport - The layered transport to use (defaults
+ * to none).
+ * @property {object} protocol - The Thrift Protocol to use (defaults to
+ * TBinaryProtocol).
+ * @property {object} cls - The Thrift Service class generated by the IDL
+ * Compiler for the service.
+ * @property {object} handler - The handler methods for the Thrift Service.
+ */
+
+/**
+ * Creates a Thrift server which can serve static files and/or one or
+ * more Thrift Services.
+ * @param {ThriftWebServerOptions} options - The server configuration.
+ * @returns {object} - The Thrift server.
+ */
+exports.createWebServer = function(options) {
+ var baseDir = options.staticFilePath;
+ var contentTypesByExtension = {
+ '.txt': 'text/plain',
+ '.html': 'text/html',
+ '.css': 'text/css',
+ '.xml': 'application/xml',
+ '.json': 'application/json',
+ '.js': 'application/javascript',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.gif': 'image/gif',
+ '.png': 'image/png',
+ '.svg': 'image/svg+xml'
+ };
+
+ //Setup all of the services
+ var services = options.services;
+ for (svc in services) {
+ var svcObj = services[svc];
+ var processor = svcObj.cls.Processor || svcObj.cls;
+ svcObj.processor = new processor(svcObj.handler);
+ svcObj.transport = svcObj.transport ? svcObj.transport : TBufferedTransport;
+ svcObj.protocol = svcObj.protocol ? svcObj.protocol : TBinaryProtocol;
+ }
+
+ //Handle POST methods (TXHRTransport)
+ function processPost(request, response) {
+ var uri = url.parse(request.url).pathname;
+ var svc = services[uri];
+ if (!svc) {
+ //TODO: add support for non Thrift posts
+ response.writeHead(500);
+ response.end();
+ return;
+ }
+
+ request.on('data', svc.transport.receiver(function(transportWithData) {
+ var input = new svc.protocol(transportWithData);
+ var output = new svc.protocol(new svc.transport(undefined, function(buf) {
+ try {
+ response.writeHead(200);
+ response.end(buf);
+ } catch (err) {
+ response.writeHead(500);
+ response.end();
+ }
+ }));
+
+ try {
+ svc.processor.process(input, output);
+ transportWithData.commitPosition();
+ }
+ catch (err) {
+ if (err instanceof TTransport.InputBufferUnderrunError) {
+ transportWithData.rollbackPosition();
+ }
+ else {
+ response.writeHead(500);
+ response.end();
+ }
+ }
+ }));
+ }
+
+ //Handle GET methods (Static Page Server)
+ function processGet(request, response) {
+ //Undefined or empty base directory means do not serve static files
+ if (!baseDir || "" == baseDir) {
+ response.writeHead(404);
+ response.end();
+ return;
+ }
+ //Locate the file requested
+ var uri = url.parse(request.url).pathname;
+ var filename = path.join(baseDir, uri);
+ fs.exists(filename, function(exists) {
+ if(!exists) {
+ response.writeHead(404);
+ response.end();
+ return;
+ }
+
+ if (fs.statSync(filename).isDirectory()) {
+ filename += '/index.html';
+ }
+
+ fs.readFile(filename, "binary", function(err, file) {
+ if (err) {
+ response.writeHead(500);
+ response.end(err + "\n");
+ return;
+ }
+ var headers = {};
+ var contentType = contentTypesByExtension[path.extname(filename)];
+ if (contentType) {
+ headers["Content-Type"] = contentType;
+ }
+ response.writeHead(200, headers);
+ response.write(file, "binary");
+ response.end();
+ });
+ });
+ }
+
+ //Handle WebSocket calls (TWebSocketTransport)
+ function processWS(data, socket) {
+ var svc = services[Object.keys(services)[0]];
+ //TODO: add multiservice support (maybe multiplexing is the answer for both XHR and WS?)
+
+ svc.transport.receiver(function(transportWithData) {
+ var input = new svc.protocol(transportWithData);
+ var output = new svc.protocol(new svc.transport(undefined, function(buf) {
+ try {
+ var frame = wsFrame.encode(buf);
+ socket.write(frame);
+ } catch (err) {
+ //TODO: Add better error processing
+ }
+ }));
+
+ try {
+ svc.processor.process(input, output);
+ transportWithData.commitPosition();
+ }
+ catch (err) {
+ if (err instanceof TTransport.InputBufferUnderrunError) {
+ transportWithData.rollbackPosition();
+ }
+ else {
+ //TODO: Add better error processing
+ }
+ }
+ })(data);
+ }
+
+ //Create the server (HTTP or HTTPS)
+ var server = null;
+ if (options.tlsOptions) {
+ server = https.createServer(options.tlsOptions);
+ } else {
+ server = http.createServer();
+ }
+
+ //Wire up listeners for request(GET[files]), request(POST[XHR]), upgrade(WebSocket)
+ server.on('request', function(request, response) {
+ if (request.method === 'POST') {
+ processPost(request, response);
+ } else if (request.method === 'GET') {
+ processGet(request, response);
+ } else {
+ response.writeHead(500);
+ response.end();
+ }
+ }).on('upgrade', function(request, socket, head) {
+ var hash = crypto.createHash("sha1")
+ hash.update(request.headers['sec-websocket-key'] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
+ socket.write("HTTP/1.1 101 Switching Protocols\r\n" +
+ "Upgrade: websocket\r\n" +
+ "Connection: Upgrade\r\n" +
+ "Sec-WebSocket-Accept: " + hash.digest("base64") + "\r\n" +
+ "\r\n");
+ socket.on('data', function(frame) {
+ do {
+ var result = wsFrame.decode(frame);
+ processWS(result.data, socket);
+ frame = result.nextFrame;
+ } while (frame);
+ });
+ });
+
+ //Return the server
+ return server;
+};
+
+
+
+
+
+