THRIFT-2397 Add CORS and CSP support for JavaScript and Node.js libraries
Patch: Randy Abernethy
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 @@
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 @@
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 @@
<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 @@
</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 @@
}
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 @@
}
}
- 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);
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 @@
* @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 @@
* @example
* var protocol = new Thrift.Protocol(transport);
*/
-Thrift.Protocol = function(transport) {
+Thrift.TJSONProtocol = Thrift.Protocol = function(transport) {
this.transport = transport;
};
@@ -977,16 +977,8 @@
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 @@
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());
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 @@
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 @@
-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.
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 @@
// 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 @@
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 @@
/**
* @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 @@
* @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 @@
//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 POST methods (TXHRTransport)
- function processPost(request, response) {
+ //Handle OPTIONS method (CORS support)
+ ///////////////////////////////////////////////////
+ function processOptions(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);
+ //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) {
+ 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 @@
}
//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 @@
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 @@
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 @@
}
//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 @@
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();