123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- /*
- GET /note
- GET /note/:id
- POST /note
- PUT /note/:id
- DELETE /note/:id
- */
- HTTP = Package.http && Package.http.HTTP || {};
- // Primary local test scope
- _methodHTTP = {};
- _methodHTTP.methodHandlers = {};
- _methodHTTP.methodTree = {};
- // This could be changed eg. could allow larger data chunks than 1.000.000
- // 5mb = 5 * 1024 * 1024 = 5242880;
- HTTP.methodsMaxDataLength = 5242880; //1e6;
- _methodHTTP.nameFollowsConventions = function(name) {
- // Check that name is string, not a falsy or empty
- return name && name === '' + name && name !== '';
- };
- _methodHTTP.getNameList = function(name) {
- // Remove leading and trailing slashes and make command array
- name = name && name.replace(/^\//g, '') || ''; // /^\/|\/$/g
- // TODO: Get the format from the url - eg.: "/list/45.json" format should be
- // set in this function by splitting the last list item by . and have format
- // as the last item. How should we toggle:
- // "/list/45/item.name.json" and "/list/45/item.name"?
- // We would either have to check all known formats or allways determin the "."
- // as an extension. Resolving in "json" and "name" as handed format - the user
- // Could simply just add the format as a parametre? or be explicit about
- // naming
- return name && name.split('/') || [];
- };
- // Merge two arrays one containing keys and one values
- _methodHTTP.createObject = function(keys, values) {
- var result = {};
- if (keys && values) {
- for (var i = 0; i < keys.length; i++) {
- result[keys[i]] = values[i] && decodeURIComponent(values[i]) || '';
- }
- }
- return result;
- };
- _methodHTTP.addToMethodTree = function(methodName) {
- var list = _methodHTTP.getNameList(methodName);
- var name = '/';
- // Contains the list of params names
- var params = [];
- var currentMethodTree = _methodHTTP.methodTree;
- for (var i = 0; i < list.length; i++) {
- // get the key name
- var key = list[i];
- // Check if it expects a value
- if (key[0] === ':') {
- // This is a value
- params.push(key.slice(1));
- key = ':value';
- }
- name += key + '/';
- // Set the key into the method tree
- if (typeof currentMethodTree[key] === 'undefined') {
- currentMethodTree[key] = {};
- }
- // Dig deeper
- currentMethodTree = currentMethodTree[key];
- }
- if (_.isEmpty(currentMethodTree[':ref'])) {
- currentMethodTree[':ref'] = {
- name: name,
- params: params
- };
- }
- return currentMethodTree[':ref'];
- };
- // This method should be optimized for speed since its called on allmost every
- // http call to the server so we return null as soon as we know its not a method
- _methodHTTP.getMethod = function(name) {
- // Check if the
- if (!_methodHTTP.nameFollowsConventions(name)) {
- return null;
- }
- var list = _methodHTTP.getNameList(name);
- // Check if we got a correct list
- if (!list || !list.length) {
- return null;
- }
- // Set current refernce in the _methodHTTP.methodTree
- var currentMethodTree = _methodHTTP.methodTree;
- // Buffer for values to hand on later
- var values = [];
- // Iterate over the method name and check if its found in the method tree
- for (var i = 0; i < list.length; i++) {
- // get the key name
- var key = list[i];
- // We expect to find the key or :value if not we break
- if (typeof currentMethodTree[key] !== 'undefined' ||
- typeof currentMethodTree[':value'] !== 'undefined') {
- // We got a result now check if its a value
- if (typeof currentMethodTree[key] === 'undefined') {
- // Push the value
- values.push(key);
- // Set the key to :value to dig deeper
- key = ':value';
- }
- } else {
- // Break - method call not found
- return null;
- }
- // Dig deeper
- currentMethodTree = currentMethodTree[key];
- }
- // Extract reference pointer
- var reference = currentMethodTree && currentMethodTree[':ref'];
- if (typeof reference !== 'undefined') {
- return {
- name: reference.name,
- params: _methodHTTP.createObject(reference.params, values),
- handle: _methodHTTP.methodHandlers[reference.name]
- };
- } else {
- // Did not get any reference to the method
- return null;
- }
- };
- // This method retrieves the userId from the token and makes sure that the token
- // is valid and not expired
- _methodHTTP.getUserId = function() {
- var self = this;
- // // Get ip, x-forwarded-for can be comma seperated ips where the first is the
- // // client ip
- // var ip = self.req.headers['x-forwarded-for'] &&
- // // Return the first item in ip list
- // self.req.headers['x-forwarded-for'].split(',')[0] ||
- // // or return the remoteAddress
- // self.req.connection.remoteAddress;
- // Check authentication
- var userToken = self.query.token;
- // Check if we are handed strings
- try {
- userToken && check(userToken, String);
- } catch(err) {
- throw new Meteor.Error(404, 'Error user token and id not of type strings, Error: ' + (err.stack || err.message));
- }
- // Set the this.userId
- if (userToken) {
- // Look up user to check if user exists and is loggedin via token
- var user = Meteor.users.findOne({
- $or: [
- {'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)},
- {'services.resume.loginTokens.token': userToken}
- ]
- });
- // TODO: check 'services.resume.loginTokens.when' to have the token expire
- // Set the userId in the scope
- return user && user._id;
- }
- return null;
- };
- // Expose the default auth for calling from custom authentication handlers.
- HTTP.defaultAuth = _methodHTTP.getUserId;
- /*
- Add default support for options
- */
- _methodHTTP.defaultOptionsHandler = function(methodObject) {
- // List of supported methods
- var allowMethods = [];
- // The final result object
- var result = {};
- // Iterate over the methods
- // XXX: We should have a way to extend this - We should have some schema model
- // for our methods...
- _.each(methodObject, function(f, methodName) {
- // Skip the stream and auth functions - they are not public / accessible
- if (methodName !== 'stream' && methodName !== 'auth') {
- // Create an empty description
- result[methodName] = { description: '', parameters: {} };
- // Add method name to headers
- allowMethods.push(methodName);
- }
- });
- // Lets play nice
- this.setStatusCode(200);
- // We have to set some allow headers here
- this.addHeader('Allow', allowMethods.join(','));
- // Return json result - Pretty print
- return JSON.stringify(result, null, '\t');
- };
- // Public interface for adding server-side http methods - if setting a method to
- // 'false' it would actually remove the method (can be used to unpublish a method)
- HTTP.methods = function(newMethods) {
- _.each(newMethods, function(func, name) {
- if (_methodHTTP.nameFollowsConventions(name)) {
- // Check if we got a function
- //if (typeof func === 'function') {
- var method = _methodHTTP.addToMethodTree(name);
- // The func is good
- if (typeof _methodHTTP.methodHandlers[method.name] !== 'undefined') {
- if (func === false) {
- // If the method is set to false then unpublish
- delete _methodHTTP.methodHandlers[method.name];
- // Delete the reference in the _methodHTTP.methodTree
- delete method.name;
- delete method.params;
- } else {
- // We should not allow overwriting - following Meteor.methods
- throw new Error('HTTP method "' + name + '" is already registered');
- }
- } else {
- // We could have a function or a object
- // The object could have:
- // '/test/': {
- // auth: function() ... returning the userId using over default
- //
- // method: function() ...
- // or
- // post: function() ...
- // put:
- // get:
- // delete:
- // head:
- // }
- /*
- We conform to the object format:
- {
- auth:
- post:
- put:
- get:
- delete:
- head:
- }
- This way we have a uniform reference
- */
- var uniObj = {};
- if (typeof func === 'function') {
- uniObj = {
- 'auth': _methodHTTP.getUserId,
- 'stream': false,
- 'POST': func,
- 'PUT': func,
- 'GET': func,
- 'DELETE': func,
- 'HEAD': func,
- 'OPTIONS': _methodHTTP.defaultOptionsHandler
- };
- } else {
- uniObj = {
- 'stream': func.stream || false,
- 'auth': func.auth || _methodHTTP.getUserId,
- 'POST': func.post || func.method,
- 'PUT': func.put || func.method,
- 'GET': func.get || func.method,
- 'DELETE': func.delete || func.method,
- 'HEAD': func.head || func.get || func.method,
- 'OPTIONS': func.options || _methodHTTP.defaultOptionsHandler
- };
- }
- // Registre the method
- _methodHTTP.methodHandlers[method.name] = uniObj; // func;
- }
- // } else {
- // // We do require a function as a function to execute later
- // throw new Error('HTTP.methods failed: ' + name + ' is not a function');
- // }
- } else {
- // We have to follow the naming spec defined in nameFollowsConventions
- throw new Error('HTTP.method "' + name + '" invalid naming of method');
- }
- });
- };
- var sendError = function(res, code, message) {
- if (code) {
- res.writeHead(code);
- } else {
- res.writeHead(500);
- }
- res.end(message);
- };
- // This handler collects the header data into either an object (if json) or the
- // raw data. The data is passed to the callback
- var requestHandler = function(req, res, callback) {
- if (typeof callback !== 'function') {
- return null;
- }
- // Container for buffers and a sum of the length
- var bufferData = [], dataLen = 0;
- // Extract the body
- req.on('data', function(data) {
- bufferData.push(data);
- dataLen += data.length;
- // We have to check the data length in order to spare the server
- if (dataLen > HTTP.methodsMaxDataLength) {
- dataLen = 0;
- bufferData = [];
- // Flood attack or faulty client
- sendError(res, 413, 'Flood attack or faulty client');
- req.connection.destroy();
- }
- });
- // When message is ready to be passed on
- req.on('end', function() {
- if (res.finished) {
- return;
- }
- // Allow the result to be undefined if so
- var result;
- // If data found the work it - either buffer or json
- if (dataLen > 0) {
- result = new Buffer(dataLen);
- // Merge the chunks into one buffer
- for (var i = 0, ln = bufferData.length, pos = 0; i < ln; i++) {
- bufferData[i].copy(result, pos);
- pos += bufferData[i].length;
- delete bufferData[i];
- }
- // Check if we could be dealing with json
- if (result[0] == 0x7b && result[1] === 0x22) {
- try {
- // Convert the body into json and extract the data object
- result = EJSON.parse(result.toString());
- } catch(err) {
- // Could not parse so we return the raw data
- }
- }
- } // Else result will be undefined
- try {
- callback(result);
- } catch(err) {
- sendError(res, 500, 'Error in requestHandler callback, Error: ' + (err.stack || err.message) );
- }
- });
- };
- // This is the simplest handler - it simply passes req stream as data to the
- // method
- var streamHandler = function(req, res, callback) {
- try {
- callback();
- } catch(err) {
- sendError(res, 500, 'Error in requestHandler callback, Error: ' + (err.stack || err.message) );
- }
- };
- /*
- Allow file uploads in cordova cfs
- */
- var setCordovaHeaders = function(request, response) {
- var origin = request.headers.origin;
- // Match http://localhost:<port> for Cordova clients in Meteor 1.3
- // and http://meteor.local for earlier versions
- if (origin && (origin === 'http://meteor.local' || /^http:\/\/localhost/.test(origin))) {
- // We need to echo the origin provided in the request
- response.setHeader("Access-Control-Allow-Origin", origin);
- response.setHeader("Access-Control-Allow-Methods", "PUT");
- response.setHeader("Access-Control-Allow-Headers", "Content-Type");
- }
- };
- // Handle the actual connection
- WebApp.connectHandlers.use(function(req, res, next) {
- // Check to se if this is a http method call
- var method = _methodHTTP.getMethod(req._parsedUrl.pathname);
- // If method is null then it wasn't and we pass the request along
- if (method === null) {
- return next();
- }
- var dataHandle = (method.handle && method.handle.stream)?streamHandler:requestHandler;
- dataHandle(req, res, function(data) {
- // If methodsHandler not found or somehow the methodshandler is not a
- // function then return a 404
- if (typeof method.handle === 'undefined') {
- sendError(res, 404, 'Error HTTP method handler "' + method.name + '" is not found');
- return;
- }
- // Set CORS headers for Meteor Cordova clients
- setCordovaHeaders(req, res);
- // Set fiber scope
- var fiberScope = {
- // Pointers to Request / Response
- req: req,
- res: res,
- // Request / Response helpers
- statusCode: 200,
- method: req.method,
- // Headers for response
- headers: {
- 'Content-Type': 'text/html' // Set default type
- },
- // Arguments
- data: data,
- query: req.query,
- params: method.params,
- // Method reference
- reference: method.name,
- methodObject: method.handle,
- _streamsWaiting: 0
- };
- // Helper functions this scope
- Fiber = Npm.require('fibers');
- runServerMethod = Fiber(function(self) {
- var result, resultBuffer;
- // We fetch methods data from methodsHandler, the handler uses the this.addItem()
- // function to populate the methods, this way we have better check control and
- // better error handling + messages
- // The scope for the user methodObject callbacks
- var thisScope = {
- // The user whos id and token was used to run this method, if set/found
- userId: null,
- // The id of the data
- _id: null,
- // Set the query params ?token=1&id=2 -> { token: 1, id: 2 }
- query: self.query,
- // Set params /foo/:name/test/:id -> { name: '', id: '' }
- params: self.params,
- // Method GET, PUT, POST, DELETE, HEAD
- method: self.method,
- // User agent
- userAgent: req.headers['user-agent'],
- // All request headers
- requestHeaders: req.headers,
- // Add the request object it self
- request: req,
- // Set the userId
- setUserId: function(id) {
- this.userId = id;
- },
- // We dont simulate / run this on the client at the moment
- isSimulation: false,
- // Run the next method in a new fiber - This is default at the moment
- unblock: function() {},
- // Set the content type in header, defaults to text/html?
- setContentType: function(type) {
- self.headers['Content-Type'] = type;
- },
- setStatusCode: function(code) {
- self.statusCode = code;
- },
- addHeader: function(key, value) {
- self.headers[key] = value;
- },
- createReadStream: function() {
- self._streamsWaiting++;
- return req;
- },
- createWriteStream: function() {
- self._streamsWaiting++;
- return res;
- },
- Error: function(err) {
- if (err instanceof Meteor.Error) {
- // Return controlled error
- sendError(res, err.error, err.message);
- } else if (err instanceof Error) {
- // Return error trace - this is not intented
- sendError(res, 503, 'Error in method "' + self.reference + '", Error: ' + (err.stack || err.message) );
- } else {
- sendError(res, 503, 'Error in method "' + self.reference + '"' );
- }
- },
- // getData: function() {
- // // XXX: TODO if we could run the request handler stuff eg.
- // // in here in a fiber sync it could be cool - and the user did
- // // not have to specify the stream=true flag?
- // }
- };
- // This function sends the final response. Depending on the
- // timing of the streaming, we might have to wait for all
- // streaming to end, or we might have to wait for this function
- // to finish after streaming ends. The checks in this function
- // and the fact that we call it twice ensure that we will always
- // send the response if we haven't sent an error response, but
- // we will not send it too early.
- function sendResponseIfDone() {
- res.statusCode = self.statusCode;
- // If no streams are waiting
- if (self._streamsWaiting === 0 &&
- (self.statusCode === 200 || self.statusCode === 206) &&
- self.done &&
- !self._responseSent &&
- !res.finished) {
- self._responseSent = true;
- res.end(resultBuffer);
- }
- }
- var methodCall = self.methodObject[self.method];
- // If the method call is set for the POST/PUT/GET or DELETE then run the
- // respective methodCall if its a function
- if (typeof methodCall === 'function') {
- // Get the userId - This is either set as a method specific handler and
- // will allways default back to the builtin getUserId handler
- try {
- // Try to set the userId
- thisScope.userId = self.methodObject.auth.apply(self);
- } catch(err) {
- sendError(res, err.error, (err.message || err.stack));
- return;
- }
- // This must be attached before there's any chance of `createReadStream`
- // or `createWriteStream` being called, which means before we do
- // `methodCall.apply` below.
- req.on('end', function() {
- self._streamsWaiting--;
- sendResponseIfDone();
- });
- // Get the result of the methodCall
- try {
- if (self.method === 'OPTIONS') {
- result = methodCall.apply(thisScope, [self.methodObject]) || '';
- } else {
- result = methodCall.apply(thisScope, [self.data]) || '';
- }
- } catch(err) {
- if (err instanceof Meteor.Error) {
- // Return controlled error
- sendError(res, err.error, err.message);
- } else {
- // Return error trace - this is not intented
- sendError(res, 503, 'Error in method "' + self.reference + '", Error: ' + (err.stack || err.message) );
- }
- return;
- }
- // Set headers
- _.each(self.headers, function(value, key) {
- // If value is defined then set the header, this allows for unsetting
- // the default content-type
- if (typeof value !== 'undefined')
- res.setHeader(key, value);
- });
- // If OK / 200 then Return the result
- if (self.statusCode === 200 || self.statusCode === 206) {
- if (self.method !== "HEAD") {
- // Return result
- if (typeof result === 'string') {
- resultBuffer = new Buffer(result);
- } else {
- resultBuffer = new Buffer(JSON.stringify(result));
- }
- // Check if user wants to overwrite content length for some reason?
- if (typeof self.headers['Content-Length'] === 'undefined') {
- self.headers['Content-Length'] = resultBuffer.length;
- }
- }
- self.done = true;
- sendResponseIfDone();
- } else {
- // Allow user to alter the status code and send a message
- sendError(res, self.statusCode, result);
- }
- } else {
- sendError(res, 404, 'Service not found');
- }
- });
- // Run http methods handler
- try {
- runServerMethod.run(fiberScope);
- } catch(err) {
- sendError(res, 500, 'Error running the server http method handler, Error: ' + (err.stack || err.message));
- }
- }); // EO Request handler
- });
|