1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/rocketchat_ynh.git synced 2024-09-03 20:16:25 +02:00
rocketchat_ynh/sources/programs/server/packages/cfs_http-methods.js
2016-04-29 16:32:48 +02:00

686 lines
80 KiB
JavaScript

(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var WebApp = Package.webapp.WebApp;
var main = Package.webapp.main;
var WebAppInternals = Package.webapp.WebAppInternals;
var _ = Package.underscore._;
var EJSON = Package.ejson.EJSON;
/* Package-scope variables */
var HTTP, _methodHTTP, Fiber, runServerMethod;
(function(){
///////////////////////////////////////////////////////////////////////
// //
// packages/cfs_http-methods/packages/cfs_http-methods.js //
// //
///////////////////////////////////////////////////////////////////////
//
(function () { // 1
// 2
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:http-methods/http.methods.server.api.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/* // 1
// 2
GET /note // 3
GET /note/:id // 4
POST /note // 5
PUT /note/:id // 6
DELETE /note/:id // 7
// 8
*/ // 9
HTTP = Package.http && Package.http.HTTP || {}; // 10
// 11
// Primary local test scope // 12
_methodHTTP = {}; // 13
// 14
// 15
_methodHTTP.methodHandlers = {}; // 16
_methodHTTP.methodTree = {}; // 17
// 18
// This could be changed eg. could allow larger data chunks than 1.000.000 // 19
// 5mb = 5 * 1024 * 1024 = 5242880; // 20
HTTP.methodsMaxDataLength = 5242880; //1e6; // 21
// 22
_methodHTTP.nameFollowsConventions = function(name) { // 23
// Check that name is string, not a falsy or empty // 24
return name && name === '' + name && name !== ''; // 25
}; // 26
// 27
// 28
_methodHTTP.getNameList = function(name) { // 29
// Remove leading and trailing slashes and make command array // 30
name = name && name.replace(/^\//g, '') || ''; // /^\/|\/$/g // 31
// TODO: Get the format from the url - eg.: "/list/45.json" format should be // 32
// set in this function by splitting the last list item by . and have format // 33
// as the last item. How should we toggle: // 34
// "/list/45/item.name.json" and "/list/45/item.name"? // 35
// We would either have to check all known formats or allways determin the "." // 36
// as an extension. Resolving in "json" and "name" as handed format - the user // 37
// Could simply just add the format as a parametre? or be explicit about // 38
// naming // 39
return name && name.split('/') || []; // 40
}; // 41
// 42
// Merge two arrays one containing keys and one values // 43
_methodHTTP.createObject = function(keys, values) { // 44
var result = {}; // 45
if (keys && values) { // 46
for (var i = 0; i < keys.length; i++) { // 47
result[keys[i]] = values[i] && decodeURIComponent(values[i]) || ''; // 48
} // 49
} // 50
return result; // 51
}; // 52
// 53
_methodHTTP.addToMethodTree = function(methodName) { // 54
var list = _methodHTTP.getNameList(methodName); // 55
var name = '/'; // 56
// Contains the list of params names // 57
var params = []; // 58
var currentMethodTree = _methodHTTP.methodTree; // 59
// 60
for (var i = 0; i < list.length; i++) { // 61
// 62
// get the key name // 63
var key = list[i]; // 64
// Check if it expects a value // 65
if (key[0] === ':') { // 66
// This is a value // 67
params.push(key.slice(1)); // 68
key = ':value'; // 69
} // 70
name += key + '/'; // 71
// 72
// Set the key into the method tree // 73
if (typeof currentMethodTree[key] === 'undefined') { // 74
currentMethodTree[key] = {}; // 75
} // 76
// 77
// Dig deeper // 78
currentMethodTree = currentMethodTree[key]; // 79
// 80
} // 81
// 82
if (_.isEmpty(currentMethodTree[':ref'])) { // 83
currentMethodTree[':ref'] = { // 84
name: name, // 85
params: params // 86
}; // 87
} // 88
// 89
return currentMethodTree[':ref']; // 90
}; // 91
// 92
// This method should be optimized for speed since its called on allmost every // 93
// http call to the server so we return null as soon as we know its not a method // 94
_methodHTTP.getMethod = function(name) { // 95
// Check if the // 96
if (!_methodHTTP.nameFollowsConventions(name)) { // 97
return null; // 98
} // 99
var list = _methodHTTP.getNameList(name); // 100
// Check if we got a correct list // 101
if (!list || !list.length) { // 102
return null; // 103
} // 104
// Set current refernce in the _methodHTTP.methodTree // 105
var currentMethodTree = _methodHTTP.methodTree; // 106
// Buffer for values to hand on later // 107
var values = []; // 108
// Iterate over the method name and check if its found in the method tree // 109
for (var i = 0; i < list.length; i++) { // 110
// get the key name // 111
var key = list[i]; // 112
// We expect to find the key or :value if not we break // 113
if (typeof currentMethodTree[key] !== 'undefined' || // 114
typeof currentMethodTree[':value'] !== 'undefined') { // 115
// We got a result now check if its a value // 116
if (typeof currentMethodTree[key] === 'undefined') { // 117
// Push the value // 118
values.push(key); // 119
// Set the key to :value to dig deeper // 120
key = ':value'; // 121
} // 122
// 123
} else { // 124
// Break - method call not found // 125
return null; // 126
} // 127
// 128
// Dig deeper // 129
currentMethodTree = currentMethodTree[key]; // 130
} // 131
// 132
// Extract reference pointer // 133
var reference = currentMethodTree && currentMethodTree[':ref']; // 134
if (typeof reference !== 'undefined') { // 135
return { // 136
name: reference.name, // 137
params: _methodHTTP.createObject(reference.params, values), // 138
handle: _methodHTTP.methodHandlers[reference.name] // 139
}; // 140
} else { // 141
// Did not get any reference to the method // 142
return null; // 143
} // 144
}; // 145
// 146
// This method retrieves the userId from the token and makes sure that the token // 147
// is valid and not expired // 148
_methodHTTP.getUserId = function() { // 149
var self = this; // 150
// 151
// // Get ip, x-forwarded-for can be comma seperated ips where the first is the // 152
// // client ip // 153
// var ip = self.req.headers['x-forwarded-for'] && // 154
// // Return the first item in ip list // 155
// self.req.headers['x-forwarded-for'].split(',')[0] || // 156
// // or return the remoteAddress // 157
// self.req.connection.remoteAddress; // 158
// 159
// Check authentication // 160
var userToken = self.query.token; // 161
// 162
// Check if we are handed strings // 163
try { // 164
userToken && check(userToken, String); // 165
} catch(err) { // 166
throw new Meteor.Error(404, 'Error user token and id not of type strings, Error: ' + (err.stack || err.message)); // 167
} // 168
// 169
// Set the this.userId // 170
if (userToken) { // 171
// Look up user to check if user exists and is loggedin via token // 172
var user = Meteor.users.findOne({ // 173
$or: [ // 174
{'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 175
{'services.resume.loginTokens.token': userToken} // 176
] // 177
}); // 178
// TODO: check 'services.resume.loginTokens.when' to have the token expire // 179
// 180
// Set the userId in the scope // 181
return user && user._id; // 182
} // 183
// 184
return null; // 185
}; // 186
// 187
// Expose the default auth for calling from custom authentication handlers. // 188
HTTP.defaultAuth = _methodHTTP.getUserId; // 189
// 190
/* // 191
// 192
Add default support for options // 193
// 194
*/ // 195
_methodHTTP.defaultOptionsHandler = function(methodObject) { // 196
// List of supported methods // 197
var allowMethods = []; // 198
// The final result object // 199
var result = {}; // 200
// 201
// Iterate over the methods // 202
// XXX: We should have a way to extend this - We should have some schema model // 203
// for our methods... // 204
_.each(methodObject, function(f, methodName) { // 205
// Skip the stream and auth functions - they are not public / accessible // 206
if (methodName !== 'stream' && methodName !== 'auth') { // 207
// 208
// Create an empty description // 209
result[methodName] = { description: '', parameters: {} }; // 210
// Add method name to headers // 211
allowMethods.push(methodName); // 212
// 213
} // 214
}); // 215
// 216
// Lets play nice // 217
this.setStatusCode(200); // 218
// 219
// We have to set some allow headers here // 220
this.addHeader('Allow', allowMethods.join(',')); // 221
// 222
// Return json result - Pretty print // 223
return JSON.stringify(result, null, '\t'); // 224
}; // 225
// 226
// Public interface for adding server-side http methods - if setting a method to // 227
// 'false' it would actually remove the method (can be used to unpublish a method) // 228
HTTP.methods = function(newMethods) { // 229
_.each(newMethods, function(func, name) { // 230
if (_methodHTTP.nameFollowsConventions(name)) { // 231
// Check if we got a function // 232
//if (typeof func === 'function') { // 233
var method = _methodHTTP.addToMethodTree(name); // 234
// The func is good // 235
if (typeof _methodHTTP.methodHandlers[method.name] !== 'undefined') { // 236
if (func === false) { // 237
// If the method is set to false then unpublish // 238
delete _methodHTTP.methodHandlers[method.name]; // 239
// Delete the reference in the _methodHTTP.methodTree // 240
delete method.name; // 241
delete method.params; // 242
} else { // 243
// We should not allow overwriting - following Meteor.methods // 244
throw new Error('HTTP method "' + name + '" is already registered'); // 245
} // 246
} else { // 247
// We could have a function or a object // 248
// The object could have: // 249
// '/test/': { // 250
// auth: function() ... returning the userId using over default // 251
// // 252
// method: function() ... // 253
// or // 254
// post: function() ... // 255
// put: // 256
// get: // 257
// delete: // 258
// head: // 259
// } // 260
// 261
/* // 262
We conform to the object format: // 263
{ // 264
auth: // 265
post: // 266
put: // 267
get: // 268
delete: // 269
head: // 270
} // 271
This way we have a uniform reference // 272
*/ // 273
// 274
var uniObj = {}; // 275
if (typeof func === 'function') { // 276
uniObj = { // 277
'auth': _methodHTTP.getUserId, // 278
'stream': false, // 279
'POST': func, // 280
'PUT': func, // 281
'GET': func, // 282
'DELETE': func, // 283
'HEAD': func, // 284
'OPTIONS': _methodHTTP.defaultOptionsHandler // 285
}; // 286
} else { // 287
uniObj = { // 288
'stream': func.stream || false, // 289
'auth': func.auth || _methodHTTP.getUserId, // 290
'POST': func.post || func.method, // 291
'PUT': func.put || func.method, // 292
'GET': func.get || func.method, // 293
'DELETE': func.delete || func.method, // 294
'HEAD': func.head || func.get || func.method, // 295
'OPTIONS': func.options || _methodHTTP.defaultOptionsHandler // 296
}; // 297
} // 298
// 299
// Registre the method // 300
_methodHTTP.methodHandlers[method.name] = uniObj; // func; // 301
// 302
} // 303
// } else { // 304
// // We do require a function as a function to execute later // 305
// throw new Error('HTTP.methods failed: ' + name + ' is not a function'); // 306
// } // 307
} else { // 308
// We have to follow the naming spec defined in nameFollowsConventions // 309
throw new Error('HTTP.method "' + name + '" invalid naming of method'); // 310
} // 311
}); // 312
}; // 313
// 314
var sendError = function(res, code, message) { // 315
if (code) { // 316
res.writeHead(code); // 317
} else { // 318
res.writeHead(500); // 319
} // 320
res.end(message); // 321
}; // 322
// 323
// This handler collects the header data into either an object (if json) or the // 324
// raw data. The data is passed to the callback // 325
var requestHandler = function(req, res, callback) { // 326
if (typeof callback !== 'function') { // 327
return null; // 328
} // 329
// 330
// Container for buffers and a sum of the length // 331
var bufferData = [], dataLen = 0; // 332
// 333
// Extract the body // 334
req.on('data', function(data) { // 335
bufferData.push(data); // 336
dataLen += data.length; // 337
// 338
// We have to check the data length in order to spare the server // 339
if (dataLen > HTTP.methodsMaxDataLength) { // 340
dataLen = 0; // 341
bufferData = []; // 342
// Flood attack or faulty client // 343
sendError(res, 413, 'Flood attack or faulty client'); // 344
req.connection.destroy(); // 345
} // 346
}); // 347
// 348
// When message is ready to be passed on // 349
req.on('end', function() { // 350
if (res.finished) { // 351
return; // 352
} // 353
// 354
// Allow the result to be undefined if so // 355
var result; // 356
// 357
// If data found the work it - either buffer or json // 358
if (dataLen > 0) { // 359
result = new Buffer(dataLen); // 360
// Merge the chunks into one buffer // 361
for (var i = 0, ln = bufferData.length, pos = 0; i < ln; i++) { // 362
bufferData[i].copy(result, pos); // 363
pos += bufferData[i].length; // 364
delete bufferData[i]; // 365
} // 366
// Check if we could be dealing with json // 367
if (result[0] == 0x7b && result[1] === 0x22) { // 368
try { // 369
// Convert the body into json and extract the data object // 370
result = EJSON.parse(result.toString()); // 371
} catch(err) { // 372
// Could not parse so we return the raw data // 373
} // 374
} // 375
} // Else result will be undefined // 376
// 377
try { // 378
callback(result); // 379
} catch(err) { // 380
sendError(res, 500, 'Error in requestHandler callback, Error: ' + (err.stack || err.message) ); // 381
} // 382
}); // 383
// 384
}; // 385
// 386
// This is the simplest handler - it simply passes req stream as data to the // 387
// method // 388
var streamHandler = function(req, res, callback) { // 389
try { // 390
callback(); // 391
} catch(err) { // 392
sendError(res, 500, 'Error in requestHandler callback, Error: ' + (err.stack || err.message) ); // 393
} // 394
}; // 395
// 396
/* // 397
Allow file uploads in cordova cfs // 398
*/ // 399
var setCordovaHeaders = function(res) { // 400
res.setHeader("Access-Control-Allow-Origin", "http://meteor.local"); // 401
res.setHeader("Access-Control-Allow-Methods", "PUT"); // 402
res.setHeader("Access-Control-Allow-Headers", "Content-Type"); // 403
}; // 404
// 405
// Handle the actual connection // 406
WebApp.connectHandlers.use(function(req, res, next) { // 407
// 408
// Check to se if this is a http method call // 409
var method = _methodHTTP.getMethod(req._parsedUrl.pathname); // 410
// 411
// If method is null then it wasn't and we pass the request along // 412
if (method === null) { // 413
return next(); // 414
} // 415
// 416
var dataHandle = (method.handle && method.handle.stream)?streamHandler:requestHandler; // 417
// 418
dataHandle(req, res, function(data) { // 419
// If methodsHandler not found or somehow the methodshandler is not a // 420
// function then return a 404 // 421
if (typeof method.handle === 'undefined') { // 422
sendError(res, 404, 'Error HTTP method handler "' + method.name + '" is not found'); // 423
return; // 424
} // 425
// 426
// Set CORS headers for Meteor Cordova clients // 427
setCordovaHeaders(res); // 428
// 429
// Set fiber scope // 430
var fiberScope = { // 431
// Pointers to Request / Response // 432
req: req, // 433
res: res, // 434
// Request / Response helpers // 435
statusCode: 200, // 436
method: req.method, // 437
// Headers for response // 438
headers: { // 439
'Content-Type': 'text/html' // Set default type // 440
}, // 441
// Arguments // 442
data: data, // 443
query: req.query, // 444
params: method.params, // 445
// Method reference // 446
reference: method.name, // 447
methodObject: method.handle, // 448
_streamsWaiting: 0 // 449
}; // 450
// 451
// Helper functions this scope // 452
Fiber = Npm.require('fibers'); // 453
runServerMethod = Fiber(function(self) { // 454
var result, resultBuffer; // 455
// 456
// We fetch methods data from methodsHandler, the handler uses the this.addItem() // 457
// function to populate the methods, this way we have better check control and // 458
// better error handling + messages // 459
// 460
// The scope for the user methodObject callbacks // 461
var thisScope = { // 462
// The user whos id and token was used to run this method, if set/found // 463
userId: null, // 464
// The id of the data // 465
_id: null, // 466
// Set the query params ?token=1&id=2 -> { token: 1, id: 2 } // 467
query: self.query, // 468
// Set params /foo/:name/test/:id -> { name: '', id: '' } // 469
params: self.params, // 470
// Method GET, PUT, POST, DELETE, HEAD // 471
method: self.method, // 472
// User agent // 473
userAgent: req.headers['user-agent'], // 474
// All request headers // 475
requestHeaders: req.headers, // 476
// Add the request object it self // 477
request: req, // 478
// Set the userId // 479
setUserId: function(id) { // 480
this.userId = id; // 481
}, // 482
// We dont simulate / run this on the client at the moment // 483
isSimulation: false, // 484
// Run the next method in a new fiber - This is default at the moment // 485
unblock: function() {}, // 486
// Set the content type in header, defaults to text/html? // 487
setContentType: function(type) { // 488
self.headers['Content-Type'] = type; // 489
}, // 490
setStatusCode: function(code) { // 491
self.statusCode = code; // 492
}, // 493
addHeader: function(key, value) { // 494
self.headers[key] = value; // 495
}, // 496
createReadStream: function() { // 497
self._streamsWaiting++; // 498
return req; // 499
}, // 500
createWriteStream: function() { // 501
self._streamsWaiting++; // 502
return res; // 503
}, // 504
Error: function(err) { // 505
// 506
if (err instanceof Meteor.Error) { // 507
// Return controlled error // 508
sendError(res, err.error, err.message); // 509
} else if (err instanceof Error) { // 510
// Return error trace - this is not intented // 511
sendError(res, 503, 'Error in method "' + self.reference + '", Error: ' + (err.stack || err.message) ); // 512
} else { // 513
sendError(res, 503, 'Error in method "' + self.reference + '"' ); // 514
} // 515
// 516
}, // 517
// getData: function() { // 518
// // XXX: TODO if we could run the request handler stuff eg. // 519
// // in here in a fiber sync it could be cool - and the user did // 520
// // not have to specify the stream=true flag? // 521
// } // 522
}; // 523
// 524
// This function sends the final response. Depending on the // 525
// timing of the streaming, we might have to wait for all // 526
// streaming to end, or we might have to wait for this function // 527
// to finish after streaming ends. The checks in this function // 528
// and the fact that we call it twice ensure that we will always // 529
// send the response if we haven't sent an error response, but // 530
// we will not send it too early. // 531
function sendResponseIfDone() { // 532
res.statusCode = self.statusCode; // 533
// If no streams are waiting // 534
if (self._streamsWaiting === 0 && // 535
(self.statusCode === 200 || self.statusCode === 206) && // 536
self.done && // 537
!self._responseSent && // 538
!res.finished) { // 539
self._responseSent = true; // 540
res.end(resultBuffer); // 541
} // 542
} // 543
// 544
var methodCall = self.methodObject[self.method]; // 545
// 546
// If the method call is set for the POST/PUT/GET or DELETE then run the // 547
// respective methodCall if its a function // 548
if (typeof methodCall === 'function') { // 549
// 550
// Get the userId - This is either set as a method specific handler and // 551
// will allways default back to the builtin getUserId handler // 552
try { // 553
// Try to set the userId // 554
thisScope.userId = self.methodObject.auth.apply(self); // 555
} catch(err) { // 556
sendError(res, err.error, (err.message || err.stack)); // 557
return; // 558
} // 559
// 560
// This must be attached before there's any chance of `createReadStream` // 561
// or `createWriteStream` being called, which means before we do // 562
// `methodCall.apply` below. // 563
req.on('end', function() { // 564
self._streamsWaiting--; // 565
sendResponseIfDone(); // 566
}); // 567
// 568
// Get the result of the methodCall // 569
try { // 570
if (self.method === 'OPTIONS') { // 571
result = methodCall.apply(thisScope, [self.methodObject]) || ''; // 572
} else { // 573
result = methodCall.apply(thisScope, [self.data]) || ''; // 574
} // 575
} catch(err) { // 576
if (err instanceof Meteor.Error) { // 577
// Return controlled error // 578
sendError(res, err.error, err.message); // 579
} else { // 580
// Return error trace - this is not intented // 581
sendError(res, 503, 'Error in method "' + self.reference + '", Error: ' + (err.stack || err.message) ); // 582
} // 583
return; // 584
} // 585
// 586
// Set headers // 587
_.each(self.headers, function(value, key) { // 588
// If value is defined then set the header, this allows for unsetting // 589
// the default content-type // 590
if (typeof value !== 'undefined') // 591
res.setHeader(key, value); // 592
}); // 593
// 594
// If OK / 200 then Return the result // 595
if (self.statusCode === 200 || self.statusCode === 206) { // 596
// 597
if (self.method !== "HEAD") { // 598
// Return result // 599
if (typeof result === 'string') { // 600
resultBuffer = new Buffer(result); // 601
} else { // 602
resultBuffer = new Buffer(JSON.stringify(result)); // 603
} // 604
// 605
// Check if user wants to overwrite content length for some reason? // 606
if (typeof self.headers['Content-Length'] === 'undefined') { // 607
self.headers['Content-Length'] = resultBuffer.length; // 608
} // 609
// 610
} // 611
// 612
self.done = true; // 613
sendResponseIfDone(); // 614
// 615
} else { // 616
// Allow user to alter the status code and send a message // 617
sendError(res, self.statusCode, result); // 618
} // 619
// 620
} else { // 621
sendError(res, 404, 'Service not found'); // 622
} // 623
// 624
// 625
}); // 626
// Run http methods handler // 627
try { // 628
runServerMethod.run(fiberScope); // 629
} catch(err) { // 630
sendError(res, 500, 'Error running the server http method handler, Error: ' + (err.stack || err.message)); // 631
} // 632
// 633
}); // EO Request handler // 634
// 635
// 636
}); // 637
// 638
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 648
}).call(this); // 649
// 650
///////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['cfs:http-methods'] = {
HTTP: HTTP,
_methodHTTP: _methodHTTP
};
})();
//# sourceMappingURL=cfs_http-methods.js.map