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
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
<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");
</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 = {
}
var hello_svc_opt = {
- transport: TBufferedTransport,
- protocol: TJSONProtocol,
- cls: hello_svc,
+ transport: thrift.TBufferedTransport,
+ protocol: thrift.TJSONProtocol,
+ processor: hello_svc,
handler: hello_handler
};
}
}
- 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);
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
-------
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
-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.
// 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
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 = {
/**
* @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}
*/
* @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;
//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) {
}
//Handle GET methods (Static Page Server)
+ ///////////////////////////////////////////////////
function processGet(request, response) {
//Undefined or empty base directory means do not serve static files
if (!baseDir || "" == baseDir) {
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) {
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();
}
//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?)
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();