http.methods.server.api.js 20 KB

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