Fixed Non-ASCII attachment filename will crash when downloading.

Thanks to xet7 !

Fixes #2759
This commit is contained in:
Lauri Ojansivu 2021-04-29 13:26:49 +03:00
parent 843ff8eaaa
commit c2da477735
277 changed files with 30568 additions and 52 deletions

View file

@ -0,0 +1,644 @@
/*
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
});