diff options
author | Roger Meier <roger@apache.org> | 2014-03-12 09:38:42 +0100 |
---|---|---|
committer | Roger Meier <roger@apache.org> | 2014-03-12 09:38:42 +0100 |
commit | 52744eed7b8cc8b758825d2ba188933f907e07df (patch) | |
tree | d217770dfbb50dfb62781f251d005da9df30742b /lib/nodejs/lib/thrift/web_server.js | |
parent | 1f78987c49b2a44dd3b000c4e96d96244cf1cb9a (diff) | |
download | thrift-52744eed7b8cc8b758825d2ba188933f907e07df.tar.gz |
THRIFT-2397 Add CORS and CSP support for JavaScript and Node.js libraries
Patch: Randy Abernethy
Diffstat (limited to 'lib/nodejs/lib/thrift/web_server.js')
-rw-r--r-- | lib/nodejs/lib/thrift/web_server.js | 130 |
1 files changed, 104 insertions, 26 deletions
diff --git a/lib/nodejs/lib/thrift/web_server.js b/lib/nodejs/lib/thrift/web_server.js index c888a8079..a04038062 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(); |