Repository: thrift Updated Branches: refs/heads/master 1f78987c4 -> 52744eed7
THRIFT-2397 Add CORS and CSP support for JavaScript and Node.js libraries Patch: Randy Abernethy Project: http://git-wip-us.apache.org/repos/asf/thrift/repo Commit: http://git-wip-us.apache.org/repos/asf/thrift/commit/52744eed Tree: http://git-wip-us.apache.org/repos/asf/thrift/tree/52744eed Diff: http://git-wip-us.apache.org/repos/asf/thrift/diff/52744eed Branch: refs/heads/master Commit: 52744eed7b8cc8b758825d2ba188933f907e07df Parents: 1f78987 Author: Roger Meier <[email protected]> Authored: Wed Mar 12 09:38:42 2014 +0100 Committer: Roger Meier <[email protected]> Committed: Wed Mar 12 09:38:42 2014 +0100 ---------------------------------------------------------------------- lib/js/README | 20 +++-- lib/js/src/thrift.js | 22 ++---- lib/js/test/README | 23 +++--- lib/nodejs/lib/thrift/web_server.js | 130 ++++++++++++++++++++++++------- 4 files changed, 135 insertions(+), 60 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/thrift/blob/52744eed/lib/js/README ---------------------------------------------------------------------- diff --git a/lib/js/README b/lib/js/README index 07b188b..bb65050 100644 --- a/lib/js/README +++ b/lib/js/README @@ -25,7 +25,7 @@ under the License. Grunt Build ------------ -The is the base directory for the Apache Thrift JavaScript +This 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 @@ -46,7 +46,7 @@ Tree The following directories are present (some only after the grunt build): /src - The JavaScript Apache Thrift source - /doc - HTML documentation + /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 @@ -81,8 +81,8 @@ service. <script src="gen-js/hello_svc.js"></script> <script> (function() { - var transport = new Thrift.Transport("/hello"); - var protocol = new Thrift.Protocol(transport); + var transport = new Thrift.TXHRTransport("/hello"); + var protocol = new Thrift.TJSONProtocol(transport); var client = new hello_svcClient(protocol); var nameElement = document.getElementById("name_in"); var outputElement = document.getElementById("output"); @@ -98,9 +98,7 @@ service. </html> ### hello.js - Node Server - var Thrift = require('thrift'); - var TBufferedTransport = require('thrift/lib/thrift/transport').TBufferedTransport; - var TJSONProtocol = require('thrift/lib/thrift/protocol').TJSONProtocol; + var thrift = require('thrift'); var hello_svc = require('./gen-nodejs/hello_svc.js'); var hello_handler = { @@ -111,9 +109,9 @@ service. } var hello_svc_opt = { - transport: TBufferedTransport, - protocol: TJSONProtocol, - cls: hello_svc, + transport: thrift.TBufferedTransport, + protocol: thrift.TJSONProtocol, + processor: hello_svc, handler: hello_handler }; @@ -124,7 +122,7 @@ service. } } - var server = Thrift.createThriftWebServer(server_opt); + var server = Thrift.createWebServer(server_opt); var port = 9099; server.listen(port); console.log("Http/Thrift Server running on port: " + port); http://git-wip-us.apache.org/repos/asf/thrift/blob/52744eed/lib/js/src/thrift.js ---------------------------------------------------------------------- diff --git a/lib/js/src/thrift.js b/lib/js/src/thrift.js index 411eead..8fc7cd2 100644 --- a/lib/js/src/thrift.js +++ b/lib/js/src/thrift.js @@ -286,11 +286,11 @@ Thrift.TApplicationException.prototype.getCode = function() { * @example * var transport = new Thrift.TXHRTransport("http://localhost:8585"); */ -Thrift.Transport = Thrift.TXHRTransport = function(url) { +Thrift.Transport = Thrift.TXHRTransport = function(url, options) { this.url = url; this.wpos = 0; this.rpos = 0; - + this.useCORS = (options && options.useCORS); this.send_buf = ''; this.recv_buf = ''; }; @@ -683,7 +683,7 @@ Thrift.TWebSocketTransport.prototype = { * @example * var protocol = new Thrift.Protocol(transport); */ -Thrift.Protocol = function(transport) { +Thrift.TJSONProtocol = Thrift.Protocol = function(transport) { this.transport = transport; }; @@ -977,16 +977,8 @@ Thrift.Protocol.prototype = { var ch = str.charAt(i); // a single double quote: " if (ch === '\"') { escapedString += '\\\"'; // write out as: \" - } else if (ch === '\\') { // a single backslash: \ - escapedString += '\\\\'; // write out as: \\ - /* Currently escaped forward slashes break TJSONProtocol. - * As it stands, we can simply pass forward slashes into - * our strings across the wire without being escaped. - * I think this is the protocol's bug, not thrift.js - * } else if(ch === '/') { // a single forward slash: / - * escapedString += '\\/'; // write out as \/ - * } - */ + } else if (ch === '\\') { // a single backslash + escapedString += '\\\\'; // write out as double backslash } else if (ch === '\b') { // a single backspace: invisible escapedString += '\\b'; // write out as: \b" } else if (ch === '\f') { // a single formfeed: invisible @@ -1025,7 +1017,9 @@ Thrift.Protocol.prototype = { this.rstack = []; this.rpos = []; - if (typeof jQuery !== 'undefined') { + if (typeof JSON !== 'undefined' && typeof JSON.parse === 'function') { + this.robj = JSON.parse(this.transport.readAll()); + } else if (typeof jQuery !== 'undefined') { this.robj = jQuery.parseJSON(this.transport.readAll()); } else { this.robj = eval(this.transport.readAll()); http://git-wip-us.apache.org/repos/asf/thrift/blob/52744eed/lib/js/test/README ---------------------------------------------------------------------- diff --git a/lib/js/test/README b/lib/js/test/README index 6923794..9ad140e 100644 --- a/lib/js/test/README +++ b/lib/js/test/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 ------- @@ -32,14 +33,18 @@ 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. +server_https.js is the same but uses SSL/TLS. The server key +and cert are pulled from the thrift/test/keys folder. + 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 +To run the client test with the Java test server use: +$ make check (requires the Apache Thrift Java branch +and make check must have been run in thrift/lib/java +previously). + +To run the client tests with the Node servers run the grunt build in the parent js directory (see README there). Test Clients @@ -54,10 +59,10 @@ Test Clients -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 +QUnit based. test.html tests the Apache Thrift jQuery generated code (thrift -gen js:jquery). The test-nojq.html -Runs almost identical tests against normal JavaScript builds +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. +html drivers and contain the actual Apache Thrift tests. http://git-wip-us.apache.org/repos/asf/thrift/blob/52744eed/lib/nodejs/lib/thrift/web_server.js ---------------------------------------------------------------------- diff --git a/lib/nodejs/lib/thrift/web_server.js b/lib/nodejs/lib/thrift/web_server.js index c888a80..a040380 100644 --- a/lib/nodejs/lib/thrift/web_server.js +++ b/lib/nodejs/lib/thrift/web_server.js @@ -31,18 +31,14 @@ var TBinaryProtocol = require('./protocol').TBinaryProtocol; // WSFrame constructor and prototype ///////////////////////////////////////////////////////////////////// -/** Apache Thrift RPC Web Socket Frame Layout - * Conforming to RFC 6455 circa 12/2011 +/** Apache Thrift RPC Web Socket Transport + * 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 @@ -116,10 +112,24 @@ var wsFrame = { return frame; }, - /** Decodes a WebSocket frame + /** + * @class + * @name WSDecodeResult + * @property {Buffer} data - The decoded data for the first ATRPC message + * @property {Buffer} mask - The frame mask + * @property {Boolean} binEncoding - True if binary (TBinaryProtocol), + * False if text (TJSONProtocol) + * @property {Buffer} nextFrame - Multiple ATRPC messages may be sent in a + * single WebSocket frame, this Buffer contains + * any bytes remaining to be decoded + */ + + /** Decodes a WebSocket frame * * @param {Buffer} frame - The raw inbound frame * @returns {WSDecodeResult} - The decoded payload + * + * @see {@link WSDecodeResult} */ decode: function(frame) { var result = { @@ -216,14 +226,17 @@ var wsFrame = { /** * @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 + * @name WebServerOptions + * @property {string} staticFilePath - Path to serve static files from, if absent or "" + * static file service is disabled + * @property {object} 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 URI strings + * to ThriftServiceOptions objects + * @property {object} headers - An object hash mapping header strings to header value, + * strings, these headers are transmitted in response to + * static file GET operations. * @see {@link ThriftServiceOptions} */ @@ -231,19 +244,23 @@ var wsFrame = { * @class * @name ThriftServiceOptions * @property {object} transport - The layered transport to use (defaults - * to none). + * to TBufferedTransport). * @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} processor - The Thrift Service class generated by the IDL + * Compiler for the service (the "cls" key can also + * be used for this attribute). * @property {object} handler - The handler methods for the Thrift Service. + * @property {array} corsOrigins - Array of CORS origin strings to permit requests from. */ /** * Creates a Thrift server which can serve static files and/or one or * more Thrift Services. - * @param {ThriftWebServerOptions} options - The server configuration. + * @param {WebServerOptions} options - The server configuration. * @returns {object} - The Thrift server. + * + * @see {@link WebServerOptions} */ exports.createWebServer = function(options) { var baseDir = options.staticFilePath; @@ -263,25 +280,76 @@ exports.createWebServer = function(options) { //Setup all of the services var services = options.services; - for (svc in services) { - var svcObj = services[svc]; - var processor = svcObj.cls.Processor || svcObj.cls; + for (uri in services) { + var svcObj = services[uri]; + var processor = (svcObj.processor) ? (svcObj.processor.Processor || svcObj.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; } + + //Verify CORS requirements + function VerifyCORSAndSetHeaders(request, response, svc) { + if (request.headers.origin && svc.corsOrigins) { + if (svcObj.corsOrigins["*"] || svcObj.corsOrigins[request.headers.origin]) { + //Sucess, origin allowed + response.setHeader("access-control-allow-origin", request.headers.origin); + response.setHeader("access-control-allow-methods", "POST, OPTIONS"); + response.setHeader("access-control-allow-headers", "content-type, accept"); + response.setHeader("access-control-max-age", "60"); + return true; + } else { + //Failure, origin denied + return false; + } + } + //Success, CORS is not in use + return true; + } + + //Handle OPTIONS method (CORS support) + /////////////////////////////////////////////////// + function processOptions(request, response) { + var uri = url.parse(request.url).pathname; + var svc = services[uri]; + if (!svc) { + //Unsupported service + response.writeHead("403", "No Apache Thrift Service at " + uri, {}); + response.end(); + return; + } + + //Verify CORS requirements + if (VerifyCORSAndSetHeaders(request, response, svc)) { + response.writeHead("204", "No Content", {"content-length": 0}); + } else { + response.writeHead("403", "Origin " + request.headers.origin + " not allowed", {}); + } + response.end(); + } + //Handle POST methods (TXHRTransport) + /////////////////////////////////////////////////// function processPost(request, response) { + //Lookup service var uri = url.parse(request.url).pathname; var svc = services[uri]; if (!svc) { - //TODO: add support for non Thrift posts - response.writeHead(500); + response.writeHead("403", "No Apache Thrift Service at " + uri, {}); response.end(); return; } + //Verify CORS requirements + if (!VerifyCORSAndSetHeaders(request, response, svc)) { + response.writeHead("403", "Origin " + request.headers.origin + " not allowed", {}); + response.end(); + return; + } + + //Process XHR payload 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) { @@ -311,6 +379,7 @@ exports.createWebServer = function(options) { } //Handle GET methods (Static Page Server) + /////////////////////////////////////////////////// function processGet(request, response) { //Undefined or empty base directory means do not serve static files if (!baseDir || "" == baseDir) { @@ -318,7 +387,7 @@ exports.createWebServer = function(options) { response.end(); return; } - //Locate the file requested + //Locate the file requested and send it var uri = url.parse(request.url).pathname; var filename = path.join(baseDir, uri); fs.exists(filename, function(exists) { @@ -343,6 +412,9 @@ exports.createWebServer = function(options) { if (contentType) { headers["Content-Type"] = contentType; } + for (k in options.headers) { + headers[k] = options.headers[k]; + } response.writeHead(200, headers); response.write(file, "binary"); response.end(); @@ -351,6 +423,7 @@ exports.createWebServer = function(options) { } //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?) @@ -389,12 +462,17 @@ exports.createWebServer = function(options) { server = http.createServer(); } - //Wire up listeners for request(GET[files]), request(POST[XHR]), upgrade(WebSocket) + //Wire up listeners for upgrade(to WebSocket) & request methods for: + // - GET static files, + // - POST XHR Thrift services + // - OPTIONS CORS requests server.on('request', function(request, response) { if (request.method === 'POST') { processPost(request, response); } else if (request.method === 'GET') { processGet(request, response); + } else if (request.method === 'OPTIONS') { + processOptions(request, response); } else { response.writeHead(500); response.end();
