From 2a7dccc8a06a2240f785255492d04a82c669ae9b Mon Sep 17 00:00:00 2001 From: henrique Date: Fri, 7 Mar 2014 22:16:51 +0100 Subject: [PATCH] THRIFT-2355 Add SSL and Web Socket Support to Node and JavaScript Patch: Randy Abernethy --- compiler/cpp/src/generate/t_js_generator.cc | 15 +- lib/js/Gruntfile.js | 77 +++- lib/js/README | 45 ++- lib/js/src/thrift.js | 262 ++++++++++-- lib/js/test/README | 63 +++ lib/js/test/server_http.js | 4 +- lib/js/test/server_https.js | 62 +++ lib/js/test/test-async.js | 347 ++++++++++++++++ lib/js/test/test-jq.js | 3 - lib/js/test/test-nojq.js | 3 - lib/js/test/test.html | 11 +- lib/js/test/test_handler.js | 2 +- lib/js/test/testws.html | 60 +++ lib/nodejs/lib/thrift/index.js | 4 +- lib/nodejs/lib/thrift/static_server.js | 162 -------- lib/nodejs/lib/thrift/web_server.js | 427 ++++++++++++++++++++ 16 files changed, 1323 insertions(+), 224 deletions(-) create mode 100644 lib/js/test/README create mode 100644 lib/js/test/server_https.js create mode 100644 lib/js/test/test-async.js create mode 100644 lib/js/test/testws.html delete mode 100644 lib/nodejs/lib/thrift/static_server.js create mode 100644 lib/nodejs/lib/thrift/web_server.js diff --git a/compiler/cpp/src/generate/t_js_generator.cc b/compiler/cpp/src/generate/t_js_generator.cc index 06f3562a..57887493 100644 --- a/compiler/cpp/src/generate/t_js_generator.cc +++ b/compiler/cpp/src/generate/t_js_generator.cc @@ -1191,16 +1191,13 @@ void t_js_generator::generate_service_client(t_service* tservice) { f_service_ << indent() << "if (callback) {" << endl; f_service_ << indent() << " var self = this;" << endl; f_service_ << indent() << " this.output.getTransport().flush(true, function() {" << endl; - f_service_ << indent() << " if (this.readyState == 4 && this.status == 200) {" << endl; - f_service_ << indent() << " self.output.getTransport().setRecvBuffer(this.responseText);" << endl; - f_service_ << indent() << " var result = null;" << endl; - f_service_ << indent() << " try {" << endl; - f_service_ << indent() << " result = self.recv_" << funname << "();" << endl; - f_service_ << indent() << " } catch (e) {" << endl; - f_service_ << indent() << " result = e;" << endl; - f_service_ << indent() << " }" << endl; - f_service_ << indent() << " callback(result);" << endl; + f_service_ << indent() << " var result = null;" << endl; + f_service_ << indent() << " try {" << endl; + f_service_ << indent() << " result = self.recv_" << funname << "();" << endl; + f_service_ << indent() << " } catch (e) {" << endl; + f_service_ << indent() << " result = e;" << endl; f_service_ << indent() << " }" << endl; + f_service_ << indent() << " callback(result);" << endl; f_service_ << indent() << " });" << endl; f_service_ << indent() << "} else {" << endl; f_service_ << indent() << " return this.output.getTransport().flush();" << endl; diff --git a/lib/js/Gruntfile.js b/lib/js/Gruntfile.js index 9a8bb0d1..321063f4 100644 --- a/lib/js/Gruntfile.js +++ b/lib/js/Gruntfile.js @@ -40,8 +40,14 @@ module.exports = function(grunt) { 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 @@ module.exports = function(grunt) { 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 @@ module.exports = function(grunt) { 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 971f6da9..07b188ba 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 @@ KIND, either express or implied. See the License for the 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 @@ service. } } - 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 d605ab7d..411eead5 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 @@ var Thrift = { length++; } } - return length; }, @@ -266,19 +273,20 @@ Thrift.TApplicationException.prototype.getCode = function() { }; /** - * 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 @@ Thrift.Transport = function(url) { 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 @@ Thrift.Transport.prototype = { }, /** - * 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 @@ Thrift.Transport.prototype = { } 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 @@ Thrift.Transport.prototype = { * 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,8 +404,8 @@ Thrift.Transport.prototype = { }, /** - * Sets the buffer to use when receiving server responses. - * @param {string} buf - The buffer to receive server responses. + * 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; @@ -396,8 +415,7 @@ Thrift.Transport.prototype = { }, /** - * Returns true if the transport is open, in browser based JavaScript - * this function always returns true. + * Returns true if the transport is open, XHR always returns true. * @readonly * @returns {boolean} Always True. */ @@ -406,14 +424,12 @@ Thrift.Transport.prototype = { }, /** - * Opens the transport connection, in browser based JavaScript - * this function is a nop. + * Opens the transport connection, with XHR this is a nop. */ open: function() {}, /** - * Closes the transport connection, in browser based JavaScript - * this function is a nop. + * Closes the transport connection, with XHR this is a nop. */ close: function() {}, @@ -470,6 +486,193 @@ Thrift.Transport.prototype = { }; + +/** + * 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. + */ + 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 + * @readonly + * @returns {boolean} + */ + isOpen: function() { + return this.socket && this.socket.readyState == this.socket.OPEN; + }, + + /** + * Opens the transport connection + */ + 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 + */ + close: function() { + this.socket.close(); + }, + + /** + * 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; + } + +}; + /** * Initializes a Thrift JSON protocol instance. * @constructor @@ -1161,3 +1364,4 @@ Thrift.Multiplexer.prototype.createClient = function (serviceName, SCl, transpor }; + diff --git a/lib/js/test/README b/lib/js/test/README new file mode 100644 index 00000000..69237946 --- /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 623a9796..01174bc8 100644 --- a/lib/js/test/server_http.js +++ b/lib/js/test/server_http.js @@ -39,14 +39,14 @@ var ThriftTestSvcOpt = { 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 00000000..28f0585b --- /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 00000000..4935fead --- /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 ed658e44..64608fec 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 f67ea629..19f9e61d 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 8f9e7eef..91d1a97a 100755 --- a/lib/js/test/test.html +++ b/lib/js/test/test.html @@ -27,13 +27,18 @@ - + - - + + + diff --git a/lib/js/test/test_handler.js b/lib/js/test/test_handler.js index 33c8941f..17d22cf9 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 00000000..15ee1950 --- /dev/null +++ b/lib/js/test/testws.html @@ -0,0 +1,60 @@ + + + + + + Thrift Javascript Bindings: Unit Test + + + + + + + + + + + + + + + + + +

Thrift Javascript Bindings: Unit Test (ThriftTest.thrift)

+

+
+

+
+

+ Valid XHTML 1.0! +

+ + diff --git a/lib/nodejs/lib/thrift/index.js b/lib/nodejs/lib/thrift/index.js index 8499f9a5..84874643 100644 --- a/lib/nodejs/lib/thrift/index.js +++ b/lib/nodejs/lib/thrift/index.js @@ -35,8 +35,8 @@ exports.httpMiddleware = server.httpMiddleware; 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 b61bd303..00000000 --- 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 00000000..c888a807 --- /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; +}; + + + + + + -- 2.17.1