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,18 @@
# .editorconfig
# Meteor adapted EditorConfig, http://EditorConfig.org
# By RaiX 2013
root = true
[*.js]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
charset = utf-8
max_line_length = 80
indent_brace_style = 1TBS
spaces_around_operators = true
quote_type = auto
# curly_bracket_next_line = true

View file

@ -0,0 +1,2 @@
.build*
/.versions

View file

@ -0,0 +1,140 @@
{
// JSHint Meteor Configuration File
// Match the Meteor Style Guide
//
// By @raix with contributions from @aldeed and @awatson1978
// Source https://github.com/raix/Meteor-jshintrc
//
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : false, // false: Identifiers do not need to be in camelCase. We would like to enforce this except for when interacting with our api objects which use snakeCase properties.
"curly" : false, // false: Do not require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"indent" : 2, // {int} Number of spaces to use for indentation
"latedef" : false, // true: Require variables/functions to be defined before being used
"newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` & `--`
"quotmark" : false, // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // true: Require all defined variables be used
"strict" : false, // false: Do not require all functions to be run in ES5 Strict Mode
"trailing" : true, // true: Prohibit trailing whitespaces
"maxparams" : false, // {int} Max number of formal params allowed per function
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
"maxstatements" : false, // {int} Max number statements per function
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
"maxlen" : 250, // {int} Max number of characters per line
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : true, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements"
"globalstrict" : true, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"multistr" : false, // true: Tolerate multi-line strings
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
// Environments
"browser" : true, // Web Browser (window, document, etc)
"couch" : false, // CouchDB
"devel" : true, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jquery" : false, // jQuery
"mootools" : false, // MooTools
"node" : false, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
//"meteor" : false, // Meteor.js
// Legacy
"nomen" : false, // true: Prohibit dangling `_` in variables
"onevar" : false, // true: Allow only one `var` statement per function
"passfail" : false, // true: Stop on first error
"white" : false, // true: Check against strict whitespace and indentation rules
// Custom globals, from http://docs.meteor.com, in the order they appear there
"globals" : {
"Meteor": false,
"DDP": false,
"Mongo": false,
"Session": false,
"Accounts": false,
"Template": false,
"Blaze": false,
"Spacebars": false,
"Match": false,
"check": false,
"Tracker": false,
"ReactiveVar": false,
"ReactiveDict": false,
"EJSON": false,
"HTTP": false,
"Email": false,
"Assets": false,
"Package": false,
"App": false, //mobile-config.js
"cordova": false,
"Cordova": false,
"Buffer": false,
// Meteor internals
"DDPServer": false,
"global": false,
"Log": false,
"MongoInternals": false,
"process": false,
"Retry": false,
"WebApp": false,
"WebAppInternals": false,
// globals useful when creating Meteor packages
"Npm": false,
"Tinytest": false,
// common Meteor packages
"HTTP": true,
"Random": false,
"_": false, // Underscore.js
"$": false, // jQuery
// This package
"_methodHTTP": true,
"Fiber": true,
"runServerMethod": true
}
}

View file

@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
before_install:
- "curl -L http://git.io/s0Zu-w | /bin/sh"

View file

@ -0,0 +1,152 @@
# Changelog
## vCurrent
## [v0.0.30]
#### 9/9/15 by Eric Dobbertin
- Ensure we do not try to end responses twice
## [v0.0.29]
#### 29/4/15 by Eric Dobbertin
- Respond properly to 206 Range requests (thanks [cherbst](https://github.com/cherbst))
## [v0.0.28]
#### 8/4/15 by Eric Dobbertin
- Fix issue where response would not be sent when using streams
## [v0.0.26] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.26)
#### 17/12/14 by Morten Henriksen
- mbr update, remove versions.json
## [v0.0.25] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.25)
#### 17/12/14 by Morten Henriksen
- mbr update versions and fix warnings
- *Merged pull-request:* "- Added link to documentation about usage of HTTP client-side" [#22](https://github.com/zcfs/Meteor-http-methods/issues/22) ([jkaan](https://github.com/jkaan))
- *Merged pull-request:* "Typos." [#23](https://github.com/zcfs/Meteor-http-methods/issues/23) ([bradvogel](https://github.com/bradvogel))
- - Added link to documentation about usage of HTTP client-side
- *Merged pull-request:* "typo in example: this.params instead of this.param" [#21](https://github.com/zcfs/Meteor-http-methods/issues/21) ([lukasvan3l](https://github.com/lukasvan3l))
- typo in example: this.params instead of this.param
- *Merged pull-request:* ""code" is not always there" [#20](https://github.com/zcfs/Meteor-http-methods/issues/20) ([lukasvan3l](https://github.com/lukasvan3l))
- code is not always there
- 0.9.1 support
Patches by GitHub users [@jkaan](https://github.com/jkaan), [@bradvogel](https://github.com/bradvogel), [@lukasvan3l](https://github.com/lukasvan3l).
## [v0.0.23] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.23)
#### 25/08/14 by Morten Henriksen
- *Fixed bug:* "Cannot transfer a file stream over 5Mb to the server." [#17](https://github.com/zcfs/Meteor-http-methods/issues/17)
- *Merged pull-request:* "Add this.requestHeaders to readme" [#12](https://github.com/zcfs/Meteor-http-methods/issues/12) ([matthewsimo](https://github.com/matthewsimo))
- Add this.requestHeaders to readme
Patches by GitHub user [@matthewsimo](https://github.com/matthewsimo).
## [v0.0.22] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.22)
#### 25/03/14 by Morten Henriksen
- *Merged pull-request:* "Typo fixes" [#11](https://github.com/zcfs/Meteor-http-methods/issues/11) ([dandv](https://github.com/dandv))
- *Merged pull-request:* "Cosmetic fixes" [#10](https://github.com/zcfs/Meteor-http-methods/issues/10) ([dandv](https://github.com/dandv))
- Typo fixes
- Cosmetic fixes
Patches by GitHub user [@dandv](https://github.com/dandv).
## [v0.0.21] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.21)
#### 23/03/14 by Morten Henriksen
## [v0.0.20] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.20)
#### 22/03/14 by Morten Henriksen
- add check
- use collectionFS travis version force update
## [v0.0.19] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.19)
#### 22/03/14 by Morten Henriksen
- Added ability for throwing http errors
## [v0.0.18] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.18)
#### 18/03/14 by Morten Henriksen
- support HEAD requests
- Add better streaming api and fix timeout / chunk size bug
- refactor + added read stream
- Add streaming WIP
- Name tests by package
- unbreak http-publish tests when run at in one test
## [v0.0.17] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.17)
#### 22/02/14 by Morten Henriksen
- *Fixed bug:* "Binary use currently breaks json usage of data" [#6](https://github.com/zcfs/Meteor-http-methods/issues/6)
## [v0.0.16] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.16)
#### 22/02/14 by Morten Henriksen
- Fix test - params from a query string is string now
- make sure falsy turns into string
## [v0.0.15] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.15)
#### 21/02/14 by Eric Dobbertin
## [v0.0.14] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.14)
#### 20/02/14 by Eric Dobbertin
- support binary data from the request
## [v0.0.13] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.13)
#### 17/02/14 by Morten Henriksen
- Merge branch 'master' of https://github.com/zcfs/Meteor-http-methods
- Bump to version 0.0.12
## [v0.0.12] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.12)
#### 17/02/14 by Eric Dobbertin
- Add requestHeaders to method context
- Better handling of Meteor.Error's
- url decode values in params
- Allow user to set headers in custom response
## [v0.0.11] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.11)
#### 08/12/13 by Morten Henriksen
## [v0.0.10] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.10)
#### 14/11/13 by Morten Henriksen
## [v0.0.9] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.9)
#### 30/09/13 by Morten Henriksen
- Add MIT License
## [v0.0.8] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.8)
#### 28/09/13 by Morten Henriksen
- Comment feature - format selector as extension in url
## [v0.0.7] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.7)
#### 18/09/13 by Morten Henriksen
## [v0.0.6, tag: v0.0.5] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.6, tag: v0.0.5)
#### 18/09/13 by Morten Henriksen
- Added more features
## [v0.0.4] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.4)
#### 13/09/13 by Morten Henriksen
## [v0.0.3] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.3)
#### 12/09/13 by Morten Henriksen
## [v0.0.2] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.2)
#### 12/09/13 by Morten Henriksen
- *Fixed bug:* "allow empty last value in url" [#1](https://github.com/zcfs/Meteor-http-methods/issues/1)
## [v0.0.1] (https://github.com/zcfs/Meteor-http-methods/tree/v0.0.1)
#### 12/09/13 by Morten Henriksen
- Init commit

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 [@raix](https://github.com/raix), aka Morten N.O. Nørgaard Henriksen, mh@gi-software.com
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,215 @@
wekan-cfs-http-methods [![Build Status](https://travis-ci.org/CollectionFS/Meteor-http-methods.png?branch=master)](https://travis-ci.org/CollectionFS/Meteor-http-methods)
============
~~Looking for maintainers - please reach out!~~
This package is to be archived due to inability to find contributors, thanks to everyone who helped make it possible.
**If you're looking for an alternative, we highly recommend [Meteor-Files](https://github.com/VeliovGroup/Meteor-Files) by [VeliovGroup](https://github.com/VeliovGroup)**
---
Add server-side methods to the `HTTP` object your app. It's a server-side package only *- no client simulations added.*
## Usage
The `HTTP` object gets a `methods` method:
```js
HTTP.methods({
'list': function() {
return '<b>Default content type is text/html</b>';
}
});
```
## Methods scope
The methods scope contains different kinds of inputs. We can also get user details if logged in.
* `this.userId` - the user whose id and token was used to run this method, if set/found
* `this.method` - `GET`, `POST`, `PUT`, `DELETE`
* `this.query` - query params `?token=1&id=2` -> `{ token: 1, id: 2 }`
* `this.params` - set params `/foo/:name/test/:id` -> `{ name: '', id: '' }`
* `this.userAgent` - get the user agent string set in the request header
* `this.requestHeaders` - request headers object
* `this.setUserId(id)` - option for setting the `this.userId`
* `this.isSimulation` - always false on the server
* `this.unblock` - not implemented
* `this.setContentType('text/html')` - set the content type in header, defaults to `text/html`
* `this.addHeader('Content-Disposition', 'attachment; filename="name.ext"')`
* `this.setStatusCode(200)` - set the status code in the response header
* `createReadStream` - if a request, then get the read stream
* `createWriteStream` - if you want to stream data to the client
* `Error` - when streaming we have to be able to send error and close connection
* `this.request` The original request object
## Passing data via header
From the client:
```js
HTTP.post('list', {
data: { foo: 'bar' }
}, function(err, result) {
console.log('Content: ' + result.content + ' === "Hello"');
});
```
HTTP Server method:
```js
HTTP.methods({
'list': function(data) {
if (data.foo === 'bar') {
/* data we pass via the header is parsed by EJSON.parse
If not able, then it returns the raw data instead */
}
return 'Hello';
}
});
```
## Parameters
The method name or URL can be used to pass parameters to the method. The parameters are available on the server under `this.params`:
Client
```js
HTTP.post('/items/12/emails/5', function(err, result) {
console.log('Got back: ' + result.content);
});
```
Server
```js
HTTP.methods({
'/items/:itemId/emails/:emailId': function() {
// this.params.itemId === '12'
// this.params.emailId === '5'
}
});
```
## Extended usage
The `HTTP.methods` normally takes a function, but it can be set to an object for fine-grained handling.
Example:
```js
HTTP.methods({
'/hello': {
get: function(data) {},
// post: function(data) {},
// put: function(data) {},
// delete: function(data) {},
// options: function() {
// // Example of a simple options function
// this.setStatusCode(200);
// this.addHeader('Accept', 'POST,PUT');
// // The options for this restpoint
// var options = {
// POST: {
// description: 'Create an issue',
// parameters: {
// title: {
// type: 'string',
// description: 'Issue title'
// }
// }
// }
// };
// // Print the options in pretty json
// return JSON.stringify(options, null, '\t');
// },
// stream: true // flag whether to allow stream handling in the request
}
});
```
*In this example the mounted http rest point will only support the `get` method*
Example:
```js
HTTP.methods({
'/hello': {
method: function(data) {},
}
});
```
*In this example all methods `get`, `put`, `post`, `delete` will use the same function - This would be equal to setting the function directly on the http mount point*
## Authentication
The client needs the `access_token` to login in HTTP methods. *One could create a HTTP login/logout method for allowing pure external access*
Client
```js
HTTP.post('/hello', {
params: {
token: Accounts && Accounts._storedLoginToken()
}
}, function(err, result) {
console.log('Got back: ' + result.content);
});
```
Server
```js
'hello': function(data) {
if (this.userId) {
var user = Meteor.users.findOne({ _id: this.userId });
return 'Hello ' + (user && user.username || user && user.emails[0].address || 'user');
} else {
this.setStatusCode(401); // Unauthorized
}
}
```
## Using custom authentication
It's possible to make your own function to set the userId - not using the built-in token pattern.
```js
// My auth will return the userId
var myAuth = function() {
// Read the token from '/hello?token=5'
var userToken = self.query.token;
// Check the userToken before adding it to the db query
// Set the this.userId
if (userToken) {
var user = Meteor.users.findOne({ 'services.resume.loginTokens.token': userToken });
// Set the userId in the scope
return user && user._id;
}
};
HTTP.methods({
'/hello': {
auth: myAuth,
method: function(data) {
// this.userId is set by myAuth
if (this.userId) { /**/ } else { /**/ }
}
}
});
```
*The above resembles the builtin auth handler*
## Security
When buffering data instead of streaming we set the buffer limit to 5mb - This can be changed on the fly:
```js
// Set the max data length
// 5mb = 5 * 1024 * 1024 = 5242880;
HTTP.methodsMaxDataLength = 5242880;
```
## Login and logout (TODO)
These operations are not currently supported for off Meteor use - there are some security considerations.
`basic-auth` is broadly supported, but:
* password should not be sent in clear text - hash with base64?
* should be used on https connections
* Its difficult / impossible to logout a user?
`token` the current `access_token` seems to be a better solution. Better control and options to logout users. But calling the initial `login` method still requires:
* hashing of password
* use https
## HTTP Client-side usage
If you want to use the HTTP client-side functionality and find yourself having a hard time viewing all available options; these can be found on https://docs.meteor.com/#/full/http.

View file

@ -0,0 +1,6 @@
HTTP = Package.http && Package.http.HTTP || {};
// Client-side simulation is not yet implemented
HTTP.methods = function() {
throw new Error('HTTP.methods not implemented on client-side');
};

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
});

View file

@ -0,0 +1,117 @@
function equals(a, b) {
return EJSON.stringify(a) === EJSON.stringify(b);
}
Tinytest.add('http-methods - test environment', function(test) {
test.isTrue(typeof _methodHTTP !== 'undefined', 'test environment not initialized _methodHTTP');
test.isTrue(typeof HTTP !== 'undefined', 'test environment not initialized HTTP');
test.isTrue(typeof HTTP.methods !== 'undefined', 'test environment not initialized HTTP.methods');
});
Tinytest.add('http-methods - nameFollowsConventions', function(test) {
test.isFalse(_methodHTTP.nameFollowsConventions(), 'Tested methods naming convention 1');
test.isFalse(_methodHTTP.nameFollowsConventions(''), 'Tested methods naming convention 2');
test.isFalse(_methodHTTP.nameFollowsConventions({}), 'Tested methods naming convention 3');
test.isFalse(_methodHTTP.nameFollowsConventions([1]), 'Tested methods naming convention 4');
test.isFalse(_methodHTTP.nameFollowsConventions(-1), 'Tested methods naming convention 5');
test.isFalse(_methodHTTP.nameFollowsConventions(1), 'Tested methods naming convention 6');
test.isFalse(_methodHTTP.nameFollowsConventions(0.1), 'Tested methods naming convention 7');
test.isFalse(_methodHTTP.nameFollowsConventions(-0.1), 'Tested methods naming convention 8');
test.isTrue(_methodHTTP.nameFollowsConventions('/test/test'), 'Tested methods naming convention leading slash');
test.isTrue(_methodHTTP.nameFollowsConventions('test/test'), 'Tested methods naming convention');
});
Tinytest.add('http-methods - getNameList', function(test) {
test.equal(EJSON.stringify(_methodHTTP.getNameList()), '[]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('')), '[]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('/')), '[]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('//')), '["",""]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('/1/')), '["1",""]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('/1/2')), '["1","2"]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('/1/:name/2')), '["1",":name","2"]', 'Name list failed');
test.equal(EJSON.stringify(_methodHTTP.getNameList('/1//2')), '["1","","2"]', 'Name list failed');
});
Tinytest.add('http-methods - createObject', function(test) {
test.equal(EJSON.stringify(_methodHTTP.createObject()), '{}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(2, 4)), '{}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(['foo'], [])), '{"foo":""}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(['foo'], ['bar'])), '{"foo":"bar"}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(['foo'], [3])), '{"foo":"3"}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(['foo'], ['bar', 3])), '{"foo":"bar"}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(['foo', 'foo'], ['bar', 3])), '{"foo":"3"}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject([''], ['bar', 3])), '{"":"bar"}', 'createObject failed');
test.equal(EJSON.stringify(_methodHTTP.createObject(['', ''], ['bar', 3])), '{"":"3"}', 'createObject failed');
});
Tinytest.add('http-methods - addToMethodTree', function(test) {
var original = _methodHTTP.methodTree;
_methodHTTP.methodTree = {};
_methodHTTP.addToMethodTree('login');
test.equal(EJSON.stringify(_methodHTTP.methodTree), '{"login":{":ref":{"name":"/login/","params":[]}}}', 'addToMethodTree failed');
_methodHTTP.methodTree = {};
_methodHTTP.addToMethodTree('/foo/bar');
test.equal(EJSON.stringify(_methodHTTP.methodTree), '{"foo":{"bar":{":ref":{"name":"/foo/bar/","params":[]}}}}', 'addToMethodTree failed');
_methodHTTP.methodTree = {};
_methodHTTP.addToMethodTree('/foo/:name/bar');
test.equal(EJSON.stringify(_methodHTTP.methodTree), '{"foo":{":value":{"bar":{":ref":{"name":"/foo/:value/bar/","params":["name"]}}}}}', 'addToMethodTree failed');
_methodHTTP.addToMethodTree('/foo/:name/bar');
test.equal(EJSON.stringify(_methodHTTP.methodTree), '{"foo":{":value":{"bar":{":ref":{"name":"/foo/:value/bar/","params":["name"]}}}}}', 'addToMethodTree failed');
_methodHTTP.addToMethodTree('/foo/name/bar');
test.equal(EJSON.stringify(_methodHTTP.methodTree), '{"foo":{":value":{"bar":{":ref":{"name":"/foo/:value/bar/","params":["name"]}}},"name":{"bar":{":ref":{"name":"/foo/name/bar/","params":[]}}}}}', 'addToMethodTree failed');
_methodHTTP.methodTree = original;
});
Tinytest.add('http-methods - getMethod', function(test) {
// Basic tests
test.equal(EJSON.stringify(_methodHTTP.getMethod('')), 'null', 'getMethod failed');
test.equal(EJSON.stringify(_methodHTTP.getMethod('//')), 'null', 'getMethod failed');
_methodHTTP.addToMethodTree('login');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login')), '{"name":"/login/","params":{}}', 'getMethod failed');
test.equal(EJSON.stringify(_methodHTTP.getMethod('/login')), '{"name":"/login/","params":{}}', 'getMethod failed');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login/')), 'null', 'getMethod failed');
test.equal(EJSON.stringify(_methodHTTP.getMethod('/login/')), 'null', 'getMethod failed');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login/test')), 'null', 'getMethod failed');
_methodHTTP.addToMethodTree('/login/');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login')), '{"name":"/login/","params":{}}', 'getMethod failed');
//
_methodHTTP.addToMethodTree('/login/foo');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login/foo')), '{"name":"/login/foo/","params":{}}', 'getMethod failed');
_methodHTTP.addToMethodTree('/login/:name/foo');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login/bar/foo')), '{"name":"/login/:value/foo/","params":{"name":"bar"}}', 'getMethod failed');
test.equal(EJSON.stringify(_methodHTTP.getMethod('login/foo')), '{"name":"/login/foo/","params":{}}', 'getMethod failed');
});
//Test API:
//test.isFalse(v, msg)
//test.isTrue(v, msg)
//test.equal(actual, expected, message, not)
//test.length(obj, len)
//test.include(s, v)
//test.isNaN(v, msg)
//test.isUndefined(v, msg)
//test.isNotNull
//test.isNull
//test.throws(func)
//test.instanceOf(obj, klass)
//test.notEqual(actual, expected, message)
//test.runId()
//test.exception(exception)
//test.expect_fail()
//test.ok(doc)
//test.fail(doc)

View file

@ -0,0 +1,31 @@
Package.describe({
git: 'https://github.com/zcfs/Meteor-http-methods.git',
name: 'wekan-cfs-http-methods',
version: '0.0.32',
summary: 'Adds HTTP.methods RESTful'
});
Package.onUse(function(api) {
api.versionsFrom('1.0');
api.use(['webapp', 'underscore', 'ejson'], 'server');
api.use('http', { weak: true });
api.export && api.export('HTTP');
api.export && api.export('_methodHTTP', { testOnly: true });
api.addFiles('http.methods.client.api.js', 'client');
api.addFiles('http.methods.server.api.js', 'server');
});
Package.onTest(function (api) {
api.use('wekan-cfs-http-methods', ['server']);
api.use('test-helpers', 'server');
api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict',
'random', 'deps']);
api.addFiles('http.methods.tests.js', 'server');
});