From: ra Date: Thu, 24 Apr 2014 03:04:23 +0000 (-0700) Subject: THRIFT-2493:Node.js lib needs HTTP client X-Git-Url: https://source.supwisdom.com/gerrit/gitweb?a=commitdiff_plain;h=779b9ac2eff8605836a7a08151b844601dffcf53;p=common%2Fthrift.git THRIFT-2493:Node.js lib needs HTTP client Client: node Patch: Randy Abernethy Clean up of many jshint warnings/errors, jsdoc for HttpConnect, added support for https and Python to HttpConnect, added tests for HttpConnect with https and promises. --- diff --git a/lib/nodejs/examples/README.md b/lib/nodejs/examples/README.md index 0773e606..db012df6 100644 --- a/lib/nodejs/examples/README.md +++ b/lib/nodejs/examples/README.md @@ -18,6 +18,7 @@ #Generate the bindings: ../../../compiler/cpp/thrift --gen js:node user.thrift +../../../compiler/cpp/thrift --gen js:node --gen py hello.thrift #To run the user example, first start up the server in one terminal: NODE_PATH=../lib:../lib/thrift node server.js @@ -28,5 +29,6 @@ NODE_PATH=../lib:../lib/thrift node client.js #For an example using JavaScript in the browser to connect to #a node.js server look at hello.html, hello.js and hello.thrift - +#HTTP examples are provided also: httpClient.js and httpServer.js +#You can test HTTP cross platform with the httpServer.py Python server diff --git a/lib/nodejs/examples/httpClient.js b/lib/nodejs/examples/httpClient.js new file mode 100644 index 00000000..19cc0c36 --- /dev/null +++ b/lib/nodejs/examples/httpClient.js @@ -0,0 +1,23 @@ +var thrift = require('thrift'); +var helloSvc = require('./gen-nodejs/HelloSvc.js'); + +var options = { + transport: thrift.TBufferedTransport, + protocol: thrift.TJSONProtocol, + path: "/hello", + headers: {"Connection": "close"}, + https: false +}; + +var connection = thrift.createHttpConnection("localhost", 9090, options); +var client = thrift.createHttpClient(helloSvc, connection); + +connection.on("error", function(err) { + console.log("Error: " + err); +}); + +client.hello_func(function(error, result) { + console.log("Msg from server: " + result); +}); + + diff --git a/lib/nodejs/examples/httpServer.js b/lib/nodejs/examples/httpServer.js new file mode 100644 index 00000000..acae1369 --- /dev/null +++ b/lib/nodejs/examples/httpServer.js @@ -0,0 +1,31 @@ +var thrift = require('thrift'); +var helloSvc = require('./gen-nodejs/HelloSvc'); + +//ServiceHandler: Implement the hello service +var helloHandler = { + hello_func: function (result) { + console.log("Received Hello call"); + result(null, "Hello from Node.js"); + } +}; + +//ServiceOptions: The I/O stack for the service +var helloSvcOpt = { + handler: helloHandler, + processor: helloSvc, + protocol: thrift.TJSONProtocol, + transport: thrift.TBufferedTransport +}; + +//ServerOptions: Define server features +var serverOpt = { + services: { + "/hello": helloSvcOpt + } +} + +//Create and start the web server +var port = 9090; +thrift.createWebServer(serverOpt).listen(port); +console.log("Http/Thrift Server running on port: " + port); + diff --git a/lib/nodejs/examples/httpServer.py b/lib/nodejs/examples/httpServer.py new file mode 100644 index 00000000..b8ba5861 --- /dev/null +++ b/lib/nodejs/examples/httpServer.py @@ -0,0 +1,19 @@ +import sys +sys.path.append('gen-py') + +from hello import HelloSvc +from thrift.protocol import TJSONProtocol +from thrift.server import THttpServer + +class HelloSvcHandler: + def hello_func(self): + print "Hello Called" + return "hello from Python" + +processor = HelloSvc.Processor(HelloSvcHandler()) +protoFactory = TJSONProtocol.TJSONProtocolFactory() +port = 9090 +server = THttpServer.THttpServer(processor, ("localhost", port), protoFactory) +print "Python server running on port " + str(port) +server.serve() + diff --git a/lib/nodejs/lib/thrift/connection.js b/lib/nodejs/lib/thrift/connection.js index a3c2d794..aa985df6 100644 --- a/lib/nodejs/lib/thrift/connection.js +++ b/lib/nodejs/lib/thrift/connection.js @@ -152,8 +152,9 @@ var Connection = exports.Connection = function(stream, options) { client['recv_' + header.fname](message, header.mtype, dummy_seqid); } else { delete client._reqs[dummy_seqid]; - throw new thrift.TApplicationException(thrift.TApplicationExceptionType.WRONG_METHOD_NAME, - "Received a response to an unknown RPC function"); + self.emit("error", + new thrift.TApplicationException(thrift.TApplicationExceptionType.WRONG_METHOD_NAME, + "Received a response to an unknown RPC function")); } } } diff --git a/lib/nodejs/lib/thrift/http_connection.js b/lib/nodejs/lib/thrift/http_connection.js index 7eab3208..ccf882fd 100644 --- a/lib/nodejs/lib/thrift/http_connection.js +++ b/lib/nodejs/lib/thrift/http_connection.js @@ -16,18 +16,67 @@ * specific language governing permissions and limitations * under the License. */ +var util = require('util'); +var http = require('http'); +var https = require('https'); +var EventEmitter = require("events").EventEmitter; var thrift = require('./thrift'); var ttransport = require('./transport'); var tprotocol = require('./protocol'); -var http = require('http'); +/** + * @class + * @name ConnectOptions + * @property {string} transport - The Thrift layered transport to use (TBufferedTransport, etc). + * @property {string} protocol - The Thrift serialization protocol to use (TBinaryProtocol, etc.). + * @property {string} path - The URL path to POST to (e.g. "/", "/mySvc", "/thrift/quoteSvc", etc.). + * @property {object} headers - A standard Node.js header hash, an object hash containing key/value + * pairs where the key is the header name string and the value is the header value string. + * @property {boolean} https - True causes the connection to use https, otherwise http is used. + * @property {object} nodeOptions - Options passed on to node. + * @example + * //Use a connection that requires ssl/tls, closes the connection after each request, + * // uses the buffered transport layer, uses the JSON protocol and directs RPC traffic + * // to https://thrift.example.com:9090/hello + * var thrift = require('thrift'); + * var options = { + * transport: thrift.TBufferedTransport, + * protocol: thrift.TJSONProtocol, + * path: "/hello", + * headers: {"Connection": "close"}, + * https: true + * }; + * var con = thrift.createHttpConnection("thrift.example.com", 9090, options); + * var client = thrift.createHttpClient(myService, connection); + * client.myServiceFunction(); + */ +/** + * Initializes a Thrift HttpConnection instance (use createHttpConnection() rather than + * instantiating directly). + * @constructor + * @param {string} host - The host name or IP to connect to. + * @param {number} port - The TCP port to connect to. + * @param {ConnectOptions} options - The configuration options to use. + * @throws {error} Exceptions other than ttransport.InputBufferUnderrunError are rethrown + * @event {error} The "error" event is fired when a Node.js error event occurs during + * request or response processing, in which case the node error is passed on. An "error" + * event may also be fired when the connectison can not map a response back to the + * appropriate client (an internal error), generating a TApplicationException. + * @classdesc HttpConnection objects provide Thrift end point transport + * semantics implemented over the Node.js http.request() method. + * @see {@link createHttpConnection} + */ var HttpConnection = exports.HttpConnection = function(host, port, options) { + //Initialize the emitter base object + EventEmitter.call(this); + //Set configuration var self = this; this.options = options || {}; this.host = host; this.port = port; + this.https = this.options.https || false; this.transport = this.options.transport || ttransport.TBufferedTransport; this.protocol = this.options.protocol || tprotocol.TBinaryProtocol; @@ -37,9 +86,16 @@ var HttpConnection = exports.HttpConnection = function(host, port, options) { port: this.port || 80, path: this.options.path || '/', method: 'POST', - headers: this.options.headers || {}, - tls: options.tls || {}, + headers: this.options.headers || {} }; + for (var attrname in this.options.nodeOptions) { + this.nodeOptions[attrname] = this.options.nodeOptions[attrname]; + } + /*jshint -W069 */ + if (! this.nodeOptions.headers["Connection"]) { + this.nodeOptions.headers["Connection"] = "keep-alive"; + } + /*jshint +W069 */ //The sequence map is used to map seqIDs back to the // calling client in multiplexed scenarios @@ -83,8 +139,10 @@ var HttpConnection = exports.HttpConnection = function(host, port, options) { client['recv_' + header.fname](proto, header.mtype, dummy_seqid); } else { delete client._reqs[dummy_seqid]; - throw new thrift.TApplicationException(thrift.TApplicationExceptionType.WRONG_METHOD_NAME, - "Received a response to an unknown RPC function"); + self.emit("error", + new thrift.TApplicationException( + thrift.TApplicationExceptionType.WRONG_METHOD_NAME, + "Received a response to an unknown RPC function")); } } } @@ -95,7 +153,7 @@ var HttpConnection = exports.HttpConnection = function(host, port, options) { throw e; } } - }; + } //Response handler @@ -104,8 +162,8 @@ var HttpConnection = exports.HttpConnection = function(host, port, options) { var data = []; var dataLen = 0; - response.on('error', function (err) { - console.log("Error in response: " + err); + response.on('error', function (e) { + self.emit("error", e); }); response.on('data', function (chunk) { @@ -125,29 +183,55 @@ var HttpConnection = exports.HttpConnection = function(host, port, options) { }); }; }; +util.inherits(HttpConnection, EventEmitter); +/** + * Writes Thrift message data to the connection + * @param {Buffer} data - A Node.js Buffer containing the data to write + * @returns {void} No return value. + * @event {error} the "error" event is raised upon request failure passing the + * Node.js error object to the listener. + */ HttpConnection.prototype.write = function(data) { - var req = http.request(this.nodeOptions, this.responseCallback); - - req.on('error', function(e) { - throw new thrift.TApplicationException(thrift.TApplicationExceptionType.UNKNOWN, - "Request failed"); - }); - + var self = this; + self.nodeOptions.headers["Content-length"] = data.length; + var req = (self.https) ? + https.request(self.nodeOptions, self.responseCallback) : + http.request(self.nodeOptions, self.responseCallback); + req.on('error', function(err) { + self.emit("error", err); + }); req.write(data); req.end(); }; +/** + * Creates a new HttpConnection object, used by Thrift clients to connect + * to Thrift HTTP based servers. + * @param {string} host - The host name or IP to connect to. + * @param {number} port - The TCP port to connect to. + * @param {ConnectOptions} options - The configuration options to use. + * @returns {HttpConnection} The connection object. + * @see {@link ConnectOptions} + */ exports.createHttpConnection = function(host, port, options) { return new HttpConnection(host, port, options); }; +/** + * Creates a new client object for the specified Thrift service. + * @param {object} cls - The module containing the service client + * @param {HttpConnection} httpConnection - The connection to use. + * @returns {object} The client object. + * @see {@link createHttpConnection} + */ exports.createHttpClient = function(cls, httpConnection) { if (cls.Client) { cls = cls.Client; } - return httpConnection.client = + httpConnection.client = new cls(new httpConnection.transport(undefined, function(buf) {httpConnection.write(buf);}), httpConnection.protocol); + return httpConnection.client; }; diff --git a/lib/nodejs/lib/thrift/server.js b/lib/nodejs/lib/thrift/server.js index 715378c6..383d1f5a 100644 --- a/lib/nodejs/lib/thrift/server.js +++ b/lib/nodejs/lib/thrift/server.js @@ -84,7 +84,7 @@ exports.createMultiplexServer = function(processor, options) { stream.on('end', function() { stream.end(); }); - }; + } if (options.tls) { return tls.createServer(options.tls, serverImpl); diff --git a/lib/nodejs/lib/thrift/web_server.js b/lib/nodejs/lib/thrift/web_server.js index fbe4f02a..dd7ad5f3 100644 --- a/lib/nodejs/lib/thrift/web_server.js +++ b/lib/nodejs/lib/thrift/web_server.js @@ -289,7 +289,7 @@ exports.createWebServer = function(options) { //Setup all of the services var services = options.services; - for (uri in services) { + for (var uri in services) { var svcObj = services[uri]; //Setup the processor @@ -399,7 +399,7 @@ exports.createWebServer = function(options) { /////////////////////////////////////////////////// function processGet(request, response) { //Undefined or empty base directory means do not serve static files - if (!baseDir || "" == baseDir) { + if (!baseDir || "" === baseDir) { response.writeHead(404); response.end(); return; @@ -437,7 +437,7 @@ exports.createWebServer = function(options) { if (contentType) { headers["Content-Type"] = contentType; } - for (k in options.headers) { + for (var k in options.headers) { headers[k] = options.headers[k]; } response.writeHead(200, headers); @@ -512,7 +512,7 @@ exports.createWebServer = function(options) { } } //Perform upgrade - var hash = crypto.createHash("sha1") + 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" + @@ -545,7 +545,7 @@ exports.createWebServer = function(options) { } //Prepare next frame for decoding (if any) frame = result.nextFrame; - }; + } } catch(e) { console.log("TWebSocketTransport Exception: " + e); socket.destroy(); diff --git a/lib/nodejs/test/client.js b/lib/nodejs/test/client.js index a6fabfcb..8813f913 100644 --- a/lib/nodejs/test/client.js +++ b/lib/nodejs/test/client.js @@ -53,7 +53,7 @@ var options = { protocol: protocol }; -var connection = undefined; +var connection; if (program.ssl) { options.rejectUnauthorized = false; diff --git a/lib/nodejs/test/http_client.js b/lib/nodejs/test/http_client.js index 08a7d27d..14c1a29b 100644 --- a/lib/nodejs/test/http_client.js +++ b/lib/nodejs/test/http_client.js @@ -55,23 +55,22 @@ var options = { path: "/test" }; -var connection = undefined; - if (program.ssl) { - options.rejectUnauthorized = false; - connection = thrift.createHttpConnection("localhost", 9090, options); -} else { - connection = thrift.createHttpConnection("localhost", 9090, options); -} + options.nodeOptions = { rejectUnauthorized: false }; + options.https = true; +} + +var connection = thrift.createHttpConnection("localhost", 9090, options); var client = thrift.createHttpClient(ThriftTest, connection); -//connection.on('error', function(err) { -// assert(false, err); -//}); +connection.on('error', function(err) { + assert(false, err); +}); var testDriver = ThriftTestDriver; if (program.promise) { + console.log(" --Testing promise style client"); testDriver = ThriftTestDriverPromise; } testDriver(client, function (status) { diff --git a/lib/nodejs/test/http_server.js b/lib/nodejs/test/http_server.js index d8ef73f4..f12e695a 100644 --- a/lib/nodejs/test/http_server.js +++ b/lib/nodejs/test/http_server.js @@ -54,7 +54,13 @@ var SvcOpt = { protocol: protocol, transport: transport }; -var serverOpt = { services: { "/test": SvcOpt } } +var serverOpt = { services: { "/test": SvcOpt } }; +if (program.ssl) { + serverOpt.tls = { + key: fs.readFileSync(path.resolve(__dirname, 'server.key')), + cert: fs.readFileSync(path.resolve(__dirname, 'server.crt')) + }; +} thrift.createWebServer(serverOpt).listen(9090); diff --git a/lib/nodejs/test/multiplex_client.js b/lib/nodejs/test/multiplex_client.js index 6580cb57..7b582059 100644 --- a/lib/nodejs/test/multiplex_client.js +++ b/lib/nodejs/test/multiplex_client.js @@ -47,9 +47,9 @@ var options = { protocol: protocol }; -var connection = undefined; +var connection; if (program.ssl) { - options.rejectUnauthorized = false + options.rejectUnauthorized = false; connection = thrift.createSSLConnection('localhost', 9090, options); } else { connection = thrift.createConnection('localhost', 9090, options); diff --git a/lib/nodejs/test/multiplex_server.js b/lib/nodejs/test/multiplex_server.js index 6331f6f9..18f9b0d7 100644 --- a/lib/nodejs/test/multiplex_server.js +++ b/lib/nodejs/test/multiplex_server.js @@ -72,7 +72,7 @@ if (program.ssl) { options.tls = { key: fs.readFileSync(path.resolve(__dirname, 'server.key')), cert: fs.readFileSync(path.resolve(__dirname, 'server.crt')) - } + }; } thrift.createMultiplexServer(processor, options).listen(9090); diff --git a/lib/nodejs/test/server.js b/lib/nodejs/test/server.js index 6f5abe6a..605c7003 100644 --- a/lib/nodejs/test/server.js +++ b/lib/nodejs/test/server.js @@ -17,11 +17,6 @@ * under the License. */ -//Server test for the following I/O stack: -// TBinaryProtocol -// TFramedTransport -// TSocket - var fs = require('fs'); var path = require('path'); var thrift = require('thrift'); @@ -61,7 +56,7 @@ if (program.ssl) { options.tls = { key: fs.readFileSync(path.resolve(__dirname, 'server.key')), cert: fs.readFileSync(path.resolve(__dirname, 'server.crt')) - } + }; } thrift.createServer(ThriftTest, handler, options).listen(9090); diff --git a/lib/nodejs/test/testAll.sh b/lib/nodejs/test/testAll.sh index 87bbb9db..9d1da3fc 100755 --- a/lib/nodejs/test/testAll.sh +++ b/lib/nodejs/test/testAll.sh @@ -96,5 +96,7 @@ testHttpClientServer json buffered || TESTOK=1 testHttpClientServer json framed || TESTOK=1 testHttpClientServer binary buffered || TESTOK=1 testHttpClientServer binary framed || TESTOK=1 +testHttpClientServer json buffered --promise || TESTOK=1 +testHttpClientServer binary framed --ssl || TESTOK=1 exit $TESTOK diff --git a/lib/nodejs/test/thrift_test_driver.js b/lib/nodejs/test/thrift_test_driver.js index a21c9c5d..02613ec6 100644 --- a/lib/nodejs/test/thrift_test_driver.js +++ b/lib/nodejs/test/thrift_test_driver.js @@ -67,6 +67,7 @@ client.testString("", function(err, response) { }); //all Languages in UTF-8 +/*jshint -W100 */ var stringTest = "Afrikaans, Alemannisch, Aragonés, العربية, مصرى, " + "Asturianu, Aymar aru, Azərbaycan, Башҡорт, Boarisch, Žemaitėška, " + "Беларуская, Беларуская (тарашкевіца), Български, Bamanankan, " + @@ -92,6 +93,7 @@ var stringTest = "Afrikaans, Alemannisch, Aragonés, العربية, مصرى, " "Türkçe, Татарча/Tatarça, Українська, اردو, Tiếng Việt, Volapük, " + "Walon, Winaray, 吴语, isiXhosa, ייִדיש, Yorùbá, Zeêuws, 中文, " + "Bân-lâm-gú, 粵語"; +/*jshint +W100 */ client.testString(stringTest, function(err, response) { assert( ! err); diff --git a/lib/nodejs/test/thrift_test_driver_promise.js b/lib/nodejs/test/thrift_test_driver_promise.js index b5c1b87f..22e2572a 100644 --- a/lib/nodejs/test/thrift_test_driver_promise.js +++ b/lib/nodejs/test/thrift_test_driver_promise.js @@ -70,6 +70,7 @@ client.testString("") }); //all Languages in UTF-8 +/*jshint -W100 */ var stringTest = "Afrikaans, Alemannisch, Aragonés, العربية, مصرى, " + "Asturianu, Aymar aru, Azərbaycan, Башҡорт, Boarisch, Žemaitėška, " + "Беларуская, Беларуская (тарашкевіца), Български, Bamanankan, " + @@ -95,6 +96,7 @@ var stringTest = "Afrikaans, Alemannisch, Aragonés, العربية, مصرى, " "Türkçe, Татарча/Tatarça, Українська, اردو, Tiếng Việt, Volapük, " + "Walon, Winaray, 吴语, isiXhosa, ייִדיש, Yorùbá, Zeêuws, 中文, " + "Bân-lâm-gú, 粵語"; +/*jshint +W100 */ client.testString(stringTest) .then(function(response) { @@ -330,7 +332,7 @@ client.testException('TException') client.testException('Xception') .then(function(response) { assert.equal(err.errorCode, 1001); - assert.equal('Xception', err.message) + assert.equal('Xception', err.message); }) .fail(function() { assert(false); @@ -365,7 +367,7 @@ client.testOneway(0, function(error, response) { client.testI32(-1) .then(function(response) { assert.equal(-1, response); - test_complete = true + test_complete = true; }) .fail(function() { assert(false); @@ -391,4 +393,4 @@ client.testOneway(0, function(error, response) { setTimeout(TestForCompletion, retry_interval); })(); -} +};