| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 | /*GET /noteGET /note/:idPOST /notePUT /note/:idDELETE /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 callbackvar 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// methodvar 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 connectionWebApp.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});
 |