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();