mirror of
https://github.com/wekan/wekan.git
synced 2025-12-17 07:50:12 +01:00
644 lines
20 KiB
JavaScript
644 lines
20 KiB
JavaScript
/*
|
|
|
|
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
|
|
|
|
|
|
});
|