]>
git.proxmox.com Git - ceph.git/blob - ceph/src/jaegertracing/thrift/lib/nodejs/lib/thrift/web_server.js
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
19 var http
= require('http');
20 var https
= require('https');
21 var url
= require("url");
22 var path
= require("path");
23 var fs
= require("fs");
24 var crypto
= require("crypto");
25 var log
= require('./log');
27 var MultiplexedProcessor
= require('./multiplexed_processor').MultiplexedProcessor
;
29 var TBufferedTransport
= require('./buffered_transport');
30 var TBinaryProtocol
= require('./binary_protocol');
31 var InputBufferUnderrunError
= require('./input_buffer_underrun_error');
33 // WSFrame constructor and prototype
34 /////////////////////////////////////////////////////////////////////
36 /** Apache Thrift RPC Web Socket Transport
37 * Frame layout conforming to RFC 6455 circa 12/2011
39 * Theoretical frame size limit is 4GB*4GB, however the Node Buffer
40 * limit is 1GB as of v0.10. The frame length encoding is also
41 * configured for a max of 4GB presently and needs to be adjusted
42 * if Node/Browsers become capabile of > 4GB frames.
44 * - FIN is 1 if the message is complete
45 * - RSV1/2/3 are always 0
46 * - Opcode is 1(TEXT) for TJSONProtocol and 2(BIN) for TBinaryProtocol
47 * - Mask Present bit is 1 sending to-server and 0 sending to-client
49 * + If < 126: then represented directly
50 * + If >=126: but within range of an unsigned 16 bit integer
51 * then Payload Len is 126 and the two following bytes store
53 * + Else: Payload Len is 127 and the following 8 bytes store the
54 * length as an unsigned 64 bit integer
55 * - Masking key is a 32 bit key only present when sending to the server
56 * - Payload follows the masking key or length
59 * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
60 * +-+-+-+-+-------+-+-------------+-------------------------------+
61 * |F|R|R|R| opcode|M| Payload len | Extended payload length |
62 * |I|S|S|S| (4) |A| (7) | (16/64) |
63 * |N|V|V|V| |S| | (if payload len==126/127) |
65 * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
66 * | Extended payload length continued, if payload len == 127 |
67 * + - - - - - - - - - - - - - - - +-------------------------------+
68 * | |Masking-key, if MASK set to 1 |
69 * +-------------------------------+-------------------------------+
70 * | Masking-key (continued) | Payload Data |
71 * +-------------------------------- - - - - - - - - - - - - - - - +
72 * : Payload Data continued ... :
73 * + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
74 * | Payload Data continued ... |
75 * +---------------------------------------------------------------+
78 /** Encodes a WebSocket frame
80 * @param {Buffer} data - The raw data to encode
81 * @param {Buffer} mask - The mask to apply when sending to server, null for no mask
82 * @param {Boolean} binEncoding - True for binary encoding, false for text encoding
83 * @returns {Buffer} - The WebSocket frame, ready to send
85 encode: function(data
, mask
, binEncoding
) {
86 var frame
= new Buffer(wsFrame
.frameSizeFromData(data
, mask
));
87 //Byte 0 - FIN & OPCODE
88 frame
[0] = wsFrame
.fin
.FIN
+
89 (binEncoding
? wsFrame
.frameOpCodes
.BIN
: wsFrame
.frameOpCodes
.TEXT
);
90 //Byte 1 or 1-3 or 1-9 - MASK FLAG & SIZE
91 var payloadOffset
= 2;
92 if (data
.length
< 0x7E) {
93 frame
[1] = data
.length
+ (mask
? wsFrame
.mask
.TO_SERVER
: wsFrame
.mask
.TO_CLIENT
);
94 } else if (data
.length
< 0xFFFF) {
95 frame
[1] = 0x7E + (mask
? wsFrame
.mask
.TO_SERVER
: wsFrame
.mask
.TO_CLIENT
);
96 frame
.writeUInt16BE(data
.length
, 2, true);
99 frame
[1] = 0x7F + (mask
? wsFrame
.mask
.TO_SERVER
: wsFrame
.mask
.TO_CLIENT
);
100 frame
.writeUInt32BE(0, 2, true);
101 frame
.writeUInt32BE(data
.length
, 6, true);
106 mask
.copy(frame
, payloadOffset
, 0, 4);
110 data
.copy(frame
, payloadOffset
);
112 wsFrame
.applyMask(frame
.slice(payloadOffset
), frame
.slice(payloadOffset
-4,payloadOffset
));
119 * @name WSDecodeResult
120 * @property {Buffer} data - The decoded data for the first ATRPC message
121 * @property {Buffer} mask - The frame mask
122 * @property {Boolean} binEncoding - True if binary (TBinaryProtocol),
123 * False if text (TJSONProtocol)
124 * @property {Buffer} nextFrame - Multiple ATRPC messages may be sent in a
125 * single WebSocket frame, this Buffer contains
126 * any bytes remaining to be decoded
127 * @property {Boolean} FIN - True is the message is complete
130 /** Decodes a WebSocket frame
132 * @param {Buffer} frame - The raw inbound frame, if this is a continuation
133 * frame it must have a mask property with the mask.
134 * @returns {WSDecodeResult} - The decoded payload
136 * @see {@link WSDecodeResult}
138 decode: function(frame
) {
147 //Byte 0 - FIN & OPCODE
148 if (wsFrame
.fin
.FIN
!= (frame
[0] & wsFrame
.fin
.FIN
)) {
151 result
.binEncoding
= (wsFrame
.frameOpCodes
.BIN
== (frame
[0] & wsFrame
.frameOpCodes
.BIN
));
152 //Byte 1 or 1-3 or 1-9 - SIZE
153 var lenByte
= (frame
[1] & 0x0000007F);
156 if (lenByte
== 0x7E) {
157 len
= frame
.readUInt16BE(2);
159 } else if (lenByte
== 0x7F) {
160 len
= frame
.readUInt32BE(6);
164 if (wsFrame
.mask
.TO_SERVER
== (frame
[1] & wsFrame
.mask
.TO_SERVER
)) {
165 result
.mask
= new Buffer(4);
166 frame
.copy(result
.mask
, 0, dataOffset
, dataOffset
+ 4);
170 result
.data
= new Buffer(len
);
171 frame
.copy(result
.data
, 0, dataOffset
, dataOffset
+len
);
173 wsFrame
.applyMask(result
.data
, result
.mask
);
176 if (frame
.length
> dataOffset
+len
) {
177 result
.nextFrame
= new Buffer(frame
.length
- (dataOffset
+len
));
178 frame
.copy(result
.nextFrame
, 0, dataOffset
+len
, frame
.length
);
180 //Don't forward control frames
181 if (frame
[0] & wsFrame
.frameOpCodes
.FINCTRL
) {
188 /** Masks/Unmasks data
190 * @param {Buffer} data - data to mask/unmask in place
191 * @param {Buffer} mask - the mask
193 applyMask: function(data
, mask
){
194 //TODO: look into xoring words at a time
195 var dataLen
= data
.length
;
196 var maskLen
= mask
.length
;
197 for (var i
= 0; i
< dataLen
; i
++) {
198 data
[i
] = data
[i
] ^ mask
[i
%maskLen
];
202 /** Computes frame size on the wire from data to be sent
204 * @param {Buffer} data - data.length is the assumed payload size
205 * @param {Boolean} mask - true if a mask will be sent (TO_SERVER)
207 frameSizeFromData: function(data
, mask
) {
209 if (data
.length
< 0x7E) {
211 } else if (data
.length
< 0xFFFF) {
214 return headerSize
+ data
.length
+ (mask
? 4 : 0);
236 // createWebServer constructor and options
237 /////////////////////////////////////////////////////////////////////
241 * @name ServerOptions
242 * @property {array} cors - Array of CORS origin strings to permit requests from.
243 * @property {string} files - Path to serve static files from, if absent or ""
244 * static file service is disabled.
245 * @property {object} headers - An object hash mapping header strings to header value
246 * strings, these headers are transmitted in response to
247 * static file GET operations.
248 * @property {object} services - An object hash mapping service URI strings
249 * to ServiceOptions objects
250 * @property {object} tls - Node.js TLS options (see: nodejs.org/api/tls.html),
251 * if not present or null regular http is used,
252 * at least a key and a cert must be defined to use SSL/TLS
253 * @see {@link ServiceOptions}
258 * @name ServiceOptions
259 * @property {object} transport - The layered transport to use (defaults
260 * to TBufferedTransport).
261 * @property {object} protocol - The serialization Protocol to use (defaults to
263 * @property {object} processor - The Thrift Service class/processor generated
264 * by the IDL Compiler for the service (the "cls"
265 * key can also be used for this attribute).
266 * @property {object} handler - The handler methods for the Thrift Service.
270 * Create a Thrift server which can serve static files and/or one or
271 * more Thrift Services.
272 * @param {ServerOptions} options - The server configuration.
273 * @returns {object} - The Apache Thrift Web Server.
275 exports
.createWebServer = function(options
) {
276 var baseDir
= options
.files
;
277 var contentTypesByExtension
= {
278 '.txt': 'text/plain',
279 '.html': 'text/html',
281 '.xml': 'application/xml',
282 '.json': 'application/json',
283 '.js': 'application/javascript',
284 '.jpg': 'image/jpeg',
285 '.jpeg': 'image/jpeg',
288 '.svg': 'image/svg+xml'
291 //Setup all of the services
292 var services
= options
.services
;
293 for (var uri
in services
) {
294 var svcObj
= services
[uri
];
296 //Setup the processor
297 if (svcObj
.processor
instanceof MultiplexedProcessor
) {
298 //Multiplex processors have pre embedded processor/handler pairs, save as is
299 svcObj
.processor
= svcObj
.processor
;
301 //For historical reasons Node.js supports processors passed in directly or via the
302 // IDL Compiler generated class housing the processor. Also, the options property
303 // for a Processor has been called both cls and processor at different times. We
304 // support any of the four possibilities here.
305 var processor
= (svcObj
.processor
) ? (svcObj
.processor
.Processor
|| svcObj
.processor
) :
306 (svcObj
.cls
.Processor
|| svcObj
.cls
);
307 //Processors can be supplied as constructed objects with handlers already embedded,
308 // if a handler is provided we construct a new processor, if not we use the processor
310 if (svcObj
.handler
) {
311 svcObj
.processor
= new processor(svcObj
.handler
);
313 svcObj
.processor
= processor
;
316 svcObj
.transport
= svcObj
.transport
? svcObj
.transport
: TBufferedTransport
;
317 svcObj
.protocol
= svcObj
.protocol
? svcObj
.protocol
: TBinaryProtocol
;
320 //Verify CORS requirements
321 function VerifyCORSAndSetHeaders(request
, response
) {
322 if (request
.headers
.origin
&& options
.cors
) {
323 if (options
.cors
["*"] || options
.cors
[request
.headers
.origin
]) {
324 //Allow, origin allowed
325 response
.setHeader("access-control-allow-origin", request
.headers
.origin
);
326 response
.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
327 response
.setHeader("access-control-allow-headers", "content-type, accept");
328 response
.setHeader("access-control-max-age", "60");
331 //Disallow, origin denied
335 //Allow, CORS is not in use
340 //Handle OPTIONS method (CORS)
341 ///////////////////////////////////////////////////
342 function processOptions(request
, response
) {
343 if (VerifyCORSAndSetHeaders(request
, response
)) {
344 response
.writeHead("204", "No Content", {"content-length": 0});
346 response
.writeHead("403", "Origin " + request
.headers
.origin
+ " not allowed", {});
352 //Handle POST methods (TXHRTransport)
353 ///////////////////////////////////////////////////
354 function processPost(request
, response
) {
356 var uri
= url
.parse(request
.url
).pathname
;
357 var svc
= services
[uri
];
359 response
.writeHead("403", "No Apache Thrift Service at " + uri
, {});
364 //Verify CORS requirements
365 if (!VerifyCORSAndSetHeaders(request
, response
)) {
366 response
.writeHead("403", "Origin " + request
.headers
.origin
+ " not allowed", {});
371 //Process XHR payload
372 request
.on('data', svc
.transport
.receiver(function(transportWithData
) {
373 var input
= new svc
.protocol(transportWithData
);
374 var output
= new svc
.protocol(new svc
.transport(undefined, function(buf
) {
376 response
.writeHead(200);
379 response
.writeHead(500);
385 svc
.processor
.process(input
, output
);
386 transportWithData
.commitPosition();
388 if (err
instanceof InputBufferUnderrunError
) {
389 transportWithData
.rollbackPosition();
391 response
.writeHead(500);
399 //Handle GET methods (Static Page Server)
400 ///////////////////////////////////////////////////
401 function processGet(request
, response
) {
402 //Undefined or empty base directory means do not serve static files
403 if (!baseDir
|| "" === baseDir
) {
404 response
.writeHead(404);
409 //Verify CORS requirements
410 if (!VerifyCORSAndSetHeaders(request
, response
)) {
411 response
.writeHead("403", "Origin " + request
.headers
.origin
+ " not allowed", {});
416 //Locate the file requested and send it
417 var uri
= url
.parse(request
.url
).pathname
;
418 var filename
= path
.resolve(path
.join(baseDir
, uri
));
420 //Ensure the basedir path is not able to be escaped
421 if (filename
.indexOf(baseDir
) != 0) {
422 response
.writeHead(400, "Invalid request path", {});
427 fs
.exists(filename
, function(exists
) {
429 response
.writeHead(404);
434 if (fs
.statSync(filename
).isDirectory()) {
435 filename
+= '/index.html';
438 fs
.readFile(filename
, "binary", function(err
, file
) {
440 response
.writeHead(500);
441 response
.end(err
+ "\n");
445 var contentType
= contentTypesByExtension
[path
.extname(filename
)];
447 headers
["Content-Type"] = contentType
;
449 for (var k
in options
.headers
) {
450 headers
[k
] = options
.headers
[k
];
452 response
.writeHead(200, headers
);
453 response
.write(file
, "binary");
460 //Handle WebSocket calls (TWebSocketTransport)
461 ///////////////////////////////////////////////////
462 function processWS(data
, socket
, svc
, binEncoding
) {
463 svc
.transport
.receiver(function(transportWithData
) {
464 var input
= new svc
.protocol(transportWithData
);
465 var output
= new svc
.protocol(new svc
.transport(undefined, function(buf
) {
467 var frame
= wsFrame
.encode(buf
, null, binEncoding
);
470 //TODO: Add better error processing
475 svc
.processor
.process(input
, output
);
476 transportWithData
.commitPosition();
479 if (err
instanceof InputBufferUnderrunError
) {
480 transportWithData
.rollbackPosition();
483 //TODO: Add better error processing
489 //Create the server (HTTP or HTTPS)
492 server
= https
.createServer(options
.tls
);
494 server
= http
.createServer();
497 //Wire up listeners for upgrade(to WebSocket) & request methods for:
498 // - GET static files,
499 // - POST XHR Thrift services
500 // - OPTIONS CORS requests
501 server
.on('request', function(request
, response
) {
502 if (request
.method
=== 'POST') {
503 processPost(request
, response
);
504 } else if (request
.method
=== 'GET') {
505 processGet(request
, response
);
506 } else if (request
.method
=== 'OPTIONS') {
507 processOptions(request
, response
);
509 response
.writeHead(500);
512 }).on('upgrade', function(request
, socket
, head
) {
516 svc
= services
[Object
.keys(services
)[0]];
518 socket
.write("HTTP/1.1 403 No Apache Thrift Service available\r\n\r\n");
522 var hash
= crypto
.createHash("sha1");
523 hash
.update(request
.headers
['sec-websocket-key'] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
524 socket
.write("HTTP/1.1 101 Switching Protocols\r\n" +
525 "Upgrade: websocket\r\n" +
526 "Connection: Upgrade\r\n" +
527 "Sec-WebSocket-Accept: " + hash
.digest("base64") + "\r\n" +
528 "Sec-WebSocket-Origin: " + request
.headers
.origin
+ "\r\n" +
529 "Sec-WebSocket-Location: ws://" + request
.headers
.host
+ request
.url
+ "\r\n" +
531 //Handle WebSocket traffic
533 socket
.on('data', function(frame
) {
536 var result
= wsFrame
.decode(frame
);
537 //Prepend any existing decoded data
540 var newData
= new Buffer(data
.length
+ result
.data
.length
);
542 result
.data
.copy(newData
, data
.length
);
543 result
.data
= newData
;
549 //If this completes a message process it
551 processWS(result
.data
, socket
, svc
, result
.binEncoding
);
555 //Prepare next frame for decoding (if any)
556 frame
= result
.nextFrame
;
559 log
.error('TWebSocketTransport Exception: ' + e
);