mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'devel' into mongo-upsert
This commit is contained in:
18
History.md
18
History.md
@@ -1,5 +1,11 @@
|
||||
## vNEXT
|
||||
|
||||
* `restrictCreationByEmail` option in `Accounts.config` to restrict new users to
|
||||
emails of specific domain (eg. only users with @meteor.com emails).
|
||||
|
||||
* Pass an index and the cursor itself to the callbacks in `cursor.forEach` and
|
||||
`cursor.map`, just like the corresponding `Array` methods. #63
|
||||
|
||||
* Better error when passing a string to {{#each}}. #722
|
||||
|
||||
* Write dates to Mongo as ISODate rather than Integer; existing data can be
|
||||
@@ -14,10 +20,22 @@
|
||||
0.6.5. (A bug prevented the 0.6.5 reimplementation of `register_extension`
|
||||
from working properly anyway.)
|
||||
|
||||
* Support using an HTTP proxy in the `meteor` command line tool. This
|
||||
allows the `update`, `deploy`, `logs`, and `mongo` commands to work
|
||||
behind a proxy. Use the standard `http_proxy` environment variable to
|
||||
specify your proxy endpoint. #429, #689, #1338
|
||||
|
||||
* Build Linux binaries on an older Linux machine. Meteor now supports
|
||||
running on Linux machines with glibc 2.9 or newer (Ubuntu 10.04+, RHEL
|
||||
and CentOS 6+, Fedora 10+, Debian 6+).
|
||||
|
||||
* Support OAuth1 services that require request token secrets as well as
|
||||
authentication token secrets. #1253
|
||||
|
||||
* Add `browser-policy` package for configuring and sending Content Security
|
||||
Policy and X-Frame-Options HTTP headers.
|
||||
|
||||
|
||||
## v0.6.5.1
|
||||
|
||||
* Fix syntax errors on lines that end with a backslash. #1326
|
||||
|
||||
@@ -62,9 +62,9 @@ that Meteor will call each time a client subscribes to the name.
|
||||
|
||||
Publish functions can return a
|
||||
[`Collection.Cursor`](#meteor_collection_cursor), in which case Meteor
|
||||
will publish that cursor's documents. You can also return an array of
|
||||
`Collection.Cursor`s, in which case Meteor will publish all of the
|
||||
cursors.
|
||||
will publish that cursor's documents to each subscribed client. You can
|
||||
also return an array of `Collection.Cursor`s, in which case Meteor will
|
||||
publish all of the cursors.
|
||||
|
||||
{{#warning}}
|
||||
If you return multiple cursors in an array, they currently must all be from
|
||||
@@ -92,16 +92,15 @@ different collections. We hope to lift this restriction in a future release.
|
||||
];
|
||||
});
|
||||
|
||||
Otherwise, the publish function should call the functions
|
||||
[`added`](#publish_added) (when a new document is added to the published record
|
||||
set), [`changed`](#publish_changed) (when some fields on a document in the
|
||||
record set are changed or cleared), and [`removed`](#publish_removed) (when
|
||||
documents are removed from the published record set) to inform subscribers about
|
||||
documents. These methods are provided by `this` in your publish function.
|
||||
|
||||
|
||||
|
||||
<!-- TODO discuss ready -->
|
||||
Alternatively, a publish function can directly control its published
|
||||
record set by calling the functions [`added`](#publish_added) (to add a
|
||||
new document to the published record set), [`changed`](#publish_changed)
|
||||
(to change or clear some fields on a document already in the published
|
||||
record set), and [`removed`](#publish_removed) (to remove documents from
|
||||
the published record set). Publish functions that use these functions
|
||||
should also call [`ready`](#publish_ready) once the initial record set
|
||||
is complete. These methods are provided by `this` in your publish
|
||||
function.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -966,6 +965,8 @@ cursor, use [`forEach`](#foreach), [`map`](#map), or [`fetch`](#fetch).
|
||||
|
||||
{{> api_box cursor_foreach}}
|
||||
|
||||
This interface is compatible with [Array.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach).
|
||||
|
||||
When called from a reactive computation, `forEach` registers dependencies on
|
||||
the matching documents.
|
||||
|
||||
@@ -981,6 +982,8 @@ Examples:
|
||||
|
||||
{{> api_box cursor_map}}
|
||||
|
||||
This interface is compatible with [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map).
|
||||
|
||||
When called from a reactive computation, `map` registers dependencies on
|
||||
the matching documents.
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ Template.api.meteor_collection = {
|
||||
options: [
|
||||
{name: "connection",
|
||||
type: "Object",
|
||||
descr: "The Meteor connection that will manage this collection. Uses the default connection if not specified. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection."
|
||||
descr: "The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling [`DDP.connect`](#ddp_connect) to specify a different server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection."
|
||||
},
|
||||
{name: "idGeneration",
|
||||
type: "String",
|
||||
@@ -675,25 +675,31 @@ Template.api.cursor_fetch = {
|
||||
|
||||
Template.api.cursor_foreach = {
|
||||
id: "foreach",
|
||||
name: "<em>cursor</em>.forEach(callback)",
|
||||
name: "<em>cursor</em>.forEach(callback, [thisArg])",
|
||||
locus: "Anywhere",
|
||||
descr: ["Call `callback` once for each matching document, sequentially and synchronously."],
|
||||
args: [
|
||||
{name: "callback",
|
||||
type: "Function",
|
||||
descr: "Function to call."}
|
||||
descr: "Function to call. It will be called with three arguments: the document, a 0-based index, and <em>cursor</em> itself."},
|
||||
{name: "thisArg",
|
||||
type: "Any",
|
||||
descr: "An object which will be the value of `this` inside `callback`."}
|
||||
]
|
||||
};
|
||||
|
||||
Template.api.cursor_map = {
|
||||
id: "map",
|
||||
name: "<em>cursor</em>.map(callback)",
|
||||
name: "<em>cursor</em>.map(callback, [thisArg])",
|
||||
locus: "Anywhere",
|
||||
descr: ["Map callback over all matching documents. Returns an Array."],
|
||||
args: [
|
||||
{name: "callback",
|
||||
type: "Function",
|
||||
descr: "Function to call."}
|
||||
descr: "Function to call. It will be called with three arguments: the document, a 0-based index, and <em>cursor</em> itself."},
|
||||
{name: "thisArg",
|
||||
type: "Any",
|
||||
descr: "An object which will be the value of `this` inside `callback`."}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1100,6 +1106,11 @@ Template.api.accounts_config = {
|
||||
name: "forbidClientAccountCreation",
|
||||
type: "Boolean",
|
||||
descr: "Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the \"Create account\" link will not be available."
|
||||
},
|
||||
{
|
||||
name: "restrictCreationByEmail",
|
||||
type: "String",
|
||||
descr: "If set, only allow new users with an email in the specified domain. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmail: 'school.edu' })`."
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -824,9 +824,9 @@ To get started, run
|
||||
$ meteor bundle myapp.tgz
|
||||
|
||||
This command will generate a fully-contained Node.js application in the form of
|
||||
a tarball. To run this application, you need to provide Node.js 0.8 and a
|
||||
a tarball. To run this application, you need to provide Node.js 0.10 and a
|
||||
MongoDB server. (The current release of Meteor has been tested with Node
|
||||
0.8.24.) You can then run the application by invoking node, specifying the HTTP
|
||||
0.10.19.) You can then run the application by invoking node, specifying the HTTP
|
||||
port for the application to listen on, and the MongoDB endpoint. If you don't
|
||||
already have a MongoDB server, we can recommend our friends at
|
||||
[MongoHQ](http://mongohq.com).
|
||||
|
||||
@@ -335,6 +335,7 @@ var toc = [
|
||||
"audit-argument-checks",
|
||||
"backbone",
|
||||
"bootstrap",
|
||||
"browser-policy",
|
||||
"coffeescript",
|
||||
"d3",
|
||||
"force-ssl",
|
||||
|
||||
@@ -22,6 +22,7 @@ and removed with:
|
||||
{{> pkg_audit_argument_checks}}
|
||||
{{> pkg_backbone}}
|
||||
{{> pkg_bootstrap}}
|
||||
{{> pkg_browser_policy}}
|
||||
{{> pkg_coffeescript}}
|
||||
{{> pkg_d3}}
|
||||
{{> pkg_force_ssl}}
|
||||
|
||||
156
docs/client/packages/browser-policy.html
Normal file
156
docs/client/packages/browser-policy.html
Normal file
@@ -0,0 +1,156 @@
|
||||
<template name="pkg_browser_policy">
|
||||
{{#better_markdown}}
|
||||
## `browser-policy`
|
||||
|
||||
The `browser-policy` package lets you set security-related policies that will be
|
||||
enforced by newer browsers. These policies help you prevent and mitigate common
|
||||
attacks like cross-site scripting and clickjacking.
|
||||
|
||||
`browser-policy` lets you configure the HTTP headers X-Frame-Options and
|
||||
Content-Security-Policy. X-Frame-Options tells the browser which websites are
|
||||
allowed to frame your app. You should only let trusted websites frame your app,
|
||||
because malicious sites could harm your users
|
||||
with <a href="https://www.owasp.org/index.php/Clickjacking">clickjacking
|
||||
attacks</a>.
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Security/CSP/Introducing_Content_Security_Policy">Content-Security-Policy</a>
|
||||
tells the browser where your app can load content from, which encourages safe
|
||||
practices and mitigates the damage of a cross-site-scripting attack.
|
||||
|
||||
For most apps, we recommend that you take the following steps when using
|
||||
`browser-policy`:
|
||||
|
||||
* Call `BrowserPolicy.enableContentSecurityPolicy()` to enable a starter policy
|
||||
for your app. With this starter policy, your app's client code will be able to
|
||||
load content (images, scripts, fonts, etc.) only from its own origin, except
|
||||
that XMLHttpRequests and WebSocket connections can go to any origin. Further,
|
||||
your app's client code will not be able to use functions such as `eval()` that
|
||||
convert strings to code.
|
||||
* You can use the functions described below to customize the content
|
||||
security policy. If your app does not need any inline Javascript such as inline
|
||||
`<script>` tags, we recommend that you modify the policy by calling
|
||||
`BrowserPolicy.disallowInlineScripts()` in server code. This will result in one
|
||||
extra round trip when your app is loaded, but will help prevent cross-site
|
||||
scripting attacks by disabling all scripts except those loaded from a `script
|
||||
src` attribute.
|
||||
* If your app does not need to be framed by other websites, call
|
||||
`BrowserPolicy.allowFramingBySameOrigin()` to help prevent clickjacking attacks.
|
||||
|
||||
Meteor determines the browser policy when the server starts up, so you should
|
||||
call `BrowserPolicy` functions in top-level application code or in
|
||||
`Meteor.startup`.
|
||||
|
||||
#### Frame options
|
||||
|
||||
You can use the following functions to specify which websites are allowed to
|
||||
frame your app:
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.disallowFraming()"}}
|
||||
Your app will never render inside a frame or iframe.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowFramingByOrigin(origin)"}}
|
||||
Your app will only render inside frames loaded by `origin`. You can only call
|
||||
this function once with a single origin, and cannot use wildcards or specify
|
||||
multiple origins that are allowed to frame your app. (This is a limitation of
|
||||
the X-Frame-Options header.) Example values of `origin` include
|
||||
"http://example.com" and "https://foo.example.com". Note that this value of the
|
||||
X-Frame-Options header is not yet supported in Chrome or Safari and will be
|
||||
ignored in those browsers.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowFramingBySameOrigin()"}}
|
||||
Your app will only render inside frames loaded by webpages on the same origin as
|
||||
your app.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowFramingByAnyOrigin()"}}
|
||||
Your app can be framed by any website.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
#### Content options
|
||||
|
||||
You can use the functions in this section to control how different types of
|
||||
content can be loaded on your site. In order to use any of these functions, you
|
||||
must first call `BrowserPolicy.enableContentSecurityPolicy()`, which enables the
|
||||
starter policy described above. This section covers additional functions that
|
||||
you can use to tighten or relax restrictions on what content your app can use.
|
||||
|
||||
You can use the following functions to adjust policies on where Javascript and
|
||||
CSS can be run:
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.allowInlineScripts()"}}
|
||||
Allows inline `<script>` tags, `javascript:` URLs, and inline event handlers.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallowInlineScripts()"}}
|
||||
Disallows inline Javascript. Calling this function results in an extra
|
||||
round-trip on page load to retrieve Meteor runtime configuration that is usually
|
||||
part of an inline script tag.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowEval()"}}
|
||||
Allows the creation of Javascript code from strings using function such as `eval()`.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallowEval()"}}
|
||||
Disallows eval and related functions.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allowInlineStyles()"}}
|
||||
Allows inline style tags and style attributes.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallowInlineStyles()"}}
|
||||
Disallows inline CSS.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
Finally, you can configure a whitelist of allowed requests that various types of
|
||||
content can make. The following functions are defined for the content types
|
||||
script, object, image, media, font, and connect.
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "BrowserPolicy.allow<ContentType>Origin(origin)"}}
|
||||
Allows this type of content to be loaded from the given origin. `origin` is a
|
||||
string and can include an optional scheme (such as `http` or `https`), an
|
||||
optional wildcard at the beginning, and an optional port which can be a
|
||||
wildcard. Examples include `example.com`, `https://*.example.com`, and
|
||||
`example.com:*`. You can call these functions multiple times with different
|
||||
origins to specify a whitelist of allowed origins.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allow<ContentType>DataUrl()"}}
|
||||
Allows this type of content to be loaded from a `data:` URL.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.allow<ContentType>SameOrigin()"}}
|
||||
Allows this type of content to be loaded from the same origin as your app.
|
||||
{{/dtdd}}
|
||||
|
||||
{{#dtdd "BrowserPolicy.disallow<ContentType>()"}}
|
||||
Disallows this type of content on your app.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
These functions are also defined for the content type `AllContent`, which is a
|
||||
shorthand for calling one of the above functions once for each content type.
|
||||
For example, if you want to allow the origin `https://foo.com` for all types of
|
||||
content but you want to disable `<object>` tags, you can call
|
||||
`BrowserPolicy.allowAllContentOrigin("https://foo.com")` followed by
|
||||
`BrowserPolicy.disallowObject()`.
|
||||
|
||||
Other examples of using the `BrowserPolicy` API:
|
||||
|
||||
* `BrowserPolicy.disallowObject()` causes the browser to disallow all
|
||||
`<object>` tags.
|
||||
* `BrowserPolicy.allowImageOrigin("https://example.com")`
|
||||
allows images to have their `src` attributes point to images served from
|
||||
`https://example.com`.
|
||||
* `BrowserPolicy.allowConnectOrigin("https://example.com")` allows XMLHttpRequest
|
||||
and WebSocket connections to `https://example.com`.
|
||||
|
||||
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
@@ -3,8 +3,11 @@
|
||||
## `random`
|
||||
|
||||
The `random` package provides several functions for generating random
|
||||
numbers. It uses a Meteor-provided random number generator that does not depend
|
||||
on the browser's facilities.
|
||||
numbers. It uses a cryptographically strong pseudorandom number generator when
|
||||
possible, but falls back to a weaker random number generator when
|
||||
cryptographically strong randomness is not available (on older browsers or on
|
||||
servers that don't have enough entropy to seed the cryptographically strong
|
||||
generator).
|
||||
|
||||
<dl class="callbacks">
|
||||
{{#dtdd "Random.id()"}}
|
||||
@@ -25,10 +28,5 @@ Returns a random string of `n` hexadecimal digits.
|
||||
{{/dtdd}}
|
||||
</dl>
|
||||
|
||||
{{#note}}
|
||||
In the current implementation, random values do not come from a
|
||||
cryptographically strong pseudorandom number generator. Future releases will
|
||||
improve this, particularly on the server.
|
||||
{{/note}}
|
||||
{{/better_markdown}}
|
||||
</template>
|
||||
|
||||
@@ -7,4 +7,3 @@ standard-app-packages
|
||||
autopublish
|
||||
insecure
|
||||
preserve-inputs
|
||||
random
|
||||
|
||||
@@ -7,4 +7,3 @@ standard-app-packages
|
||||
insecure
|
||||
jquery
|
||||
preserve-inputs
|
||||
random
|
||||
|
||||
@@ -132,9 +132,8 @@ Template.scratchpad.events({
|
||||
'click button, keyup input': function (evt) {
|
||||
var textbox = $('#scratchpad input');
|
||||
// if we clicked the button or hit enter
|
||||
if (evt.type === "click" ||
|
||||
(evt.type === "keyup" && evt.which === 13)) {
|
||||
|
||||
if ((evt.type === "click" || (evt.type === "keyup" && evt.which === 13))
|
||||
&& textbox.val()) {
|
||||
var word_id = Words.insert({player_id: Session.get('player_id'),
|
||||
game_id: game() && game()._id,
|
||||
word: textbox.val().toUpperCase(),
|
||||
|
||||
@@ -102,9 +102,12 @@ Meteor.methods({
|
||||
var word = Words.findOne(word_id);
|
||||
var game = Games.findOne(word.game_id);
|
||||
|
||||
// client and server can both check: must be at least three chars
|
||||
// long, not already used, and possible to make on the board.
|
||||
if (word.length < 3
|
||||
// client and server can both check that the game has time remaining, and
|
||||
// that the word is at least three chars, isn't already used, and is
|
||||
// possible to make on the board.
|
||||
if (game.clock === 0
|
||||
|| !word.word
|
||||
|| word.word.length < 3
|
||||
|| Words.find({game_id: word.game_id, word: word.word}).count() > 1
|
||||
|| paths_for_word(game.board, word.word).length === 0) {
|
||||
Words.update(word._id, {$set: {score: 0, state: 'bad'}});
|
||||
@@ -127,8 +130,8 @@ Meteor.methods({
|
||||
if (Meteor.isServer) {
|
||||
DICTIONARY = {};
|
||||
_.each(Assets.getText("enable2k.txt").split("\n"), function (line) {
|
||||
// Skip comment lines
|
||||
if (line.indexOf("//") !== 0) {
|
||||
// Skip blanks and comment lines
|
||||
if (line && line.indexOf("//") !== 0) {
|
||||
DICTIONARY[line] = true;
|
||||
}
|
||||
});
|
||||
|
||||
2
meteor
2
meteor
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
BUNDLE_VERSION=0.3.15
|
||||
BUNDLE_VERSION=0.3.16
|
||||
|
||||
# OS Check. Put here because here is where we download the precompiled
|
||||
# bundles that are arch specific.
|
||||
|
||||
@@ -34,8 +34,9 @@ Accounts._options = {};
|
||||
Accounts.config = function(options) {
|
||||
// validate option keys
|
||||
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation",
|
||||
"_tokenLifetimeSecs", "_tokenExpirationIntervalSecs",
|
||||
"_minTokenLifetimeSecs", "_connectionCloseDelaySecs"];
|
||||
"restrictCreationByEmail", "_tokenLifetimeSecs",
|
||||
"_tokenExpirationIntervalSecs", "_minTokenLifetimeSecs",
|
||||
"_connectionCloseDelaySecs"];
|
||||
_.each(_.keys(options), function (key) {
|
||||
if (!_.contains(VALID_KEYS, key)) {
|
||||
throw new Error("Accounts.config: Invalid key: " + key);
|
||||
|
||||
@@ -304,6 +304,34 @@ Accounts.validateNewUser = function (func) {
|
||||
validateNewUserHooks.push(func);
|
||||
};
|
||||
|
||||
// Helper function: returns false if email does not match company domain from
|
||||
// the configuration.
|
||||
var testEmailDomain = function (email) {
|
||||
var domain = Accounts._options.restrictCreationByEmail;
|
||||
return !domain || (new RegExp('@' + domain + '$', 'i')).test(email);
|
||||
};
|
||||
|
||||
// Validate new user's email or Google/Facebook/Github account's email
|
||||
Accounts.validateNewUser(function (user) {
|
||||
var domain = Accounts._options.restrictCreationByEmail;
|
||||
if (!domain)
|
||||
return true;
|
||||
|
||||
var emailIsGood = true;
|
||||
// User with password can have only one email on creation
|
||||
if (user.emails)
|
||||
emailIsGood &= testEmailDomain(user.emails[0].address);
|
||||
|
||||
// Find any email of any service and check it
|
||||
emailIsGood &= _.any(user.services, function (service) {
|
||||
return service.email && testEmailDomain(service.email);
|
||||
});
|
||||
|
||||
if (!emailIsGood)
|
||||
throw new Meteor.Error(403, "@" + domain + " email required");
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
///
|
||||
/// MANAGING USER OBJECTS
|
||||
|
||||
1
packages/browser-policy/.gitignore
vendored
Normal file
1
packages/browser-policy/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.build*
|
||||
123
packages/browser-policy/browser-policy-test.js
Normal file
123
packages/browser-policy/browser-policy-test.js
Normal file
@@ -0,0 +1,123 @@
|
||||
var cspsEqual = function (csp1, csp2) {
|
||||
var cspToObj = function (csp) {
|
||||
csp = csp.substring(0, csp.length - 1);
|
||||
var parts = _.map(csp.split("; "), function (part) {
|
||||
return part.split(" ");
|
||||
});
|
||||
var keys = _.map(parts, _.first);
|
||||
var values = _.map(parts, _.rest);
|
||||
_.each(values, function (value) {
|
||||
value.sort();
|
||||
});
|
||||
return _.object(keys, values);
|
||||
};
|
||||
|
||||
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
|
||||
};
|
||||
|
||||
Tinytest.add("browser-policy - csp", function (test) {
|
||||
var defaultCsp = "default-src 'self'; script-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"
|
||||
BrowserPolicy.enableContentSecurityPolicy(true /* enable for tests */);
|
||||
|
||||
// Default policy
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
|
||||
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true /* tests-only flag */));
|
||||
|
||||
// Redundant whitelisting (inline scripts already allowed in default policy)
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
|
||||
|
||||
// Disallow inline scripts
|
||||
BrowserPolicy.disallowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
|
||||
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Allow eval
|
||||
BrowserPolicy.allowEval();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
|
||||
|
||||
// Disallow inline styles
|
||||
BrowserPolicy.disallowInlineStyles();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
|
||||
"connect-src * 'self'; img-src data: 'self'; style-src 'self';"));
|
||||
|
||||
// Allow data: urls everywhere
|
||||
BrowserPolicy.allowAllContentDataUrl();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self' data:; script-src 'self' 'unsafe-eval' data:; " +
|
||||
"connect-src * data: 'self'; img-src data: 'self'; style-src 'self' data:;"));
|
||||
|
||||
// Disallow everything
|
||||
BrowserPolicy.disallowAllContent();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'none';"));
|
||||
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Put inline scripts back in
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'none'; script-src 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Add 'self' to all content types
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
|
||||
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Disallow all content except same-origin scripts
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowScriptSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'none'; script-src 'self';"));
|
||||
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
|
||||
|
||||
// Starting with all content same origin, disallowScript() and then allow
|
||||
// inline scripts. Result should be that that only inline scripts can execute,
|
||||
// not same-origin scripts.
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self';"));
|
||||
BrowserPolicy.disallowScript();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'none';"));
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'unsafe-inline';"));
|
||||
|
||||
// Starting with all content same origin, allow inline scripts. (Should result
|
||||
// in both same origin and inline scripts allowed.)
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
BrowserPolicy.allowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
|
||||
BrowserPolicy.disallowInlineScripts();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; script-src 'self';"));
|
||||
|
||||
// Allow same origin for all content, then disallow object entirely.
|
||||
BrowserPolicy.disallowAllContent();
|
||||
BrowserPolicy.allowAllContentSameOrigin();
|
||||
BrowserPolicy.disallowObject();
|
||||
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
|
||||
"default-src 'self'; object-src 'none';"));
|
||||
});
|
||||
|
||||
Tinytest.add("browser-policy - x-frame-options", function (test) {
|
||||
BrowserPolicy._reset();
|
||||
BrowserPolicy.disallowFraming();
|
||||
test.equal(BrowserPolicy._constructXFrameOptions(), "DENY");
|
||||
BrowserPolicy.allowFramingBySameOrigin();
|
||||
test.equal(BrowserPolicy._constructXFrameOptions(), "SAMEORIGIN");
|
||||
BrowserPolicy.allowFramingByOrigin("foo.com");
|
||||
test.equal(BrowserPolicy._constructXFrameOptions(), "ALLOW-FROM foo.com");
|
||||
test.throws(function () {
|
||||
BrowserPolicy.allowFramingByOrigin("bar.com");
|
||||
});
|
||||
BrowserPolicy.allowFramingByAnyOrigin();
|
||||
test.isFalse(BrowserPolicy._constructXFrameOptions());
|
||||
});
|
||||
284
packages/browser-policy/browser-policy.js
Normal file
284
packages/browser-policy/browser-policy.js
Normal file
@@ -0,0 +1,284 @@
|
||||
// To enable CSP, call BrowserPolicy.enableContentSecurityPolicy(). This enables
|
||||
// the following default policy:
|
||||
// No eval or other string-to-code, and content can only be loaded from the
|
||||
// same origin as the app (except for XHRs and websocket connections, which can
|
||||
// go to any origin).
|
||||
//
|
||||
// Apps should call BrowserPolicy.allowFramingBySameOrigin() to allow only
|
||||
// same-origin pages to frame their apps, if they don't explicitly want to be
|
||||
// framed by third-party sites.
|
||||
//
|
||||
// Apps should call BrowserPolicy.disallowInlineScripts() if they are not using
|
||||
// any inline script tags and are willing to accept an extra round trip on page
|
||||
// load.
|
||||
//
|
||||
// BrowserPolicy functions for tweaking CSP:
|
||||
// allowInlineScripts()
|
||||
// disallowInlineScripts(): adds extra round-trip to page load time
|
||||
// allowInlineStyles()
|
||||
// disallowInlineStyles()
|
||||
// allowEval()
|
||||
// disallowEval()
|
||||
//
|
||||
// For each type of content (script, object, image, media, font, connect,
|
||||
// style), there are the following functions:
|
||||
// allow<content type>Origin(origin): allows the type of content to be loaded
|
||||
// from the given origin
|
||||
// allow<content type>DataUrl(): allows the content to be loaded from data: URLs
|
||||
// allow<content type>SameOrigin(): allows the content to be loaded from the
|
||||
// same origin
|
||||
// disallow<content type>(): disallows this type of content all together (can't
|
||||
// be called for script)
|
||||
//
|
||||
// The following functions allow you to set rules for all types of content at
|
||||
// once:
|
||||
// allowAllContentOrigin(origin)
|
||||
// allowAllContentDataUrl()
|
||||
// allowAllContentSameOrigin()
|
||||
// disallowAllContent()
|
||||
//
|
||||
//
|
||||
// For controlling which origins can frame this app,
|
||||
// BrowserPolicy.disallowFraming()
|
||||
// BrowserPolicy.allowFramingByOrigin(origin)
|
||||
// BrowserPolicy.allowFramingBySameOrigin()
|
||||
// BrowserPolicy.allowFramingByAnyOrigin();
|
||||
|
||||
var xFrameOptions;
|
||||
var cspSrcs;
|
||||
|
||||
// CSP keywords have to be single-quoted.
|
||||
var unsafeInline = "'unsafe-inline'";
|
||||
var unsafeEval = "'unsafe-eval'";
|
||||
var selfKeyword = "'self'";
|
||||
var noneKeyword = "'none'";
|
||||
|
||||
var cspEnabled = false;
|
||||
var cspEnabledForTests = false;
|
||||
|
||||
BrowserPolicy = {};
|
||||
|
||||
// Exported for tests.
|
||||
var constructXFrameOptions = BrowserPolicy._constructXFrameOptions =
|
||||
function () {
|
||||
return xFrameOptions;
|
||||
};
|
||||
|
||||
var constructCsp = BrowserPolicy._constructCsp = function () {
|
||||
cspSrcs = cspSrcs || {};
|
||||
|
||||
var header = _.map(cspSrcs, function (srcs, directive) {
|
||||
srcs = srcs || [];
|
||||
if (_.isEmpty(srcs))
|
||||
srcs = [noneKeyword];
|
||||
var directiveCsp = _.uniq(srcs).join(" ");
|
||||
return directive + " " + directiveCsp + ";";
|
||||
});
|
||||
|
||||
header = header.join(" ");
|
||||
return header;
|
||||
};
|
||||
|
||||
var parseCsp = function (csp) {
|
||||
var policies = csp.split("; ");
|
||||
cspSrcs = {};
|
||||
_.each(policies, function (policy) {
|
||||
if (policy[policy.length - 1] === ";")
|
||||
policy = policy.substring(0, policy.length - 1);
|
||||
var srcs = policy.split(" ");
|
||||
var directive = srcs[0];
|
||||
if (_.indexOf(srcs, noneKeyword) !== -1)
|
||||
cspSrcs[directive] = null;
|
||||
else
|
||||
cspSrcs[directive] = srcs.slice(1);
|
||||
});
|
||||
|
||||
if (cspSrcs["default-src"] === undefined)
|
||||
throw new Error("Content Security Policies used with " +
|
||||
"browser-policy must specify a default-src.");
|
||||
|
||||
// Copy default-src sources to other directives.
|
||||
_.each(cspSrcs, function (sources, directive) {
|
||||
cspSrcs[directive] = _.union(sources || [], cspSrcs["default-src"] || []);
|
||||
});
|
||||
};
|
||||
|
||||
var removeCspSrc = function (directive, src) {
|
||||
cspSrcs[directive] = _.without(cspSrcs[directive] || [], src);
|
||||
};
|
||||
|
||||
var ensureDirective = function (directive) {
|
||||
throwIfNotEnabled();
|
||||
cspSrcs = cspSrcs || {};
|
||||
if (! _.has(cspSrcs, directive))
|
||||
cspSrcs[directive] = _.clone(cspSrcs["default-src"]);
|
||||
};
|
||||
|
||||
var throwIfNotEnabled = function () {
|
||||
if (! cspEnabled && ! cspEnabledForTests)
|
||||
throw new Error("Enable this function by calling "+
|
||||
"BrowserPolicy.enableContentSecurityPolicy().");
|
||||
};
|
||||
|
||||
WebApp.connectHandlers.use(function (req, res, next) {
|
||||
if (xFrameOptions)
|
||||
res.setHeader("X-Frame-Options", constructXFrameOptions());
|
||||
if (cspEnabled)
|
||||
res.setHeader("Content-Security-Policy", constructCsp());
|
||||
next();
|
||||
});
|
||||
|
||||
BrowserPolicy = _.extend(BrowserPolicy, {
|
||||
_reset: function () {
|
||||
xFrameOptions = null;
|
||||
cspSrcs = null;
|
||||
cspEnabled = false;
|
||||
},
|
||||
|
||||
allowFramingBySameOrigin: function () {
|
||||
xFrameOptions = "SAMEORIGIN";
|
||||
},
|
||||
disallowFraming: function () {
|
||||
xFrameOptions = "DENY";
|
||||
},
|
||||
// ALLOW-FROM not supported in Chrome or Safari.
|
||||
allowFramingByOrigin: function (origin) {
|
||||
// Trying to specify two allow-from throws to prevent users from
|
||||
// accidentally overwriting an allow-from origin when they think they are
|
||||
// adding multiple origins.
|
||||
if (xFrameOptions && xFrameOptions.indexOf("ALLOW-FROM") === 0)
|
||||
throw new Error("You can only specify one origin that is allowed to" +
|
||||
" frame this app.");
|
||||
xFrameOptions = "ALLOW-FROM " + origin;
|
||||
},
|
||||
allowFramingByAnyOrigin: function () {
|
||||
xFrameOptions = null;
|
||||
},
|
||||
|
||||
// _enableForTests means that you can call CSP functions, but the header won't
|
||||
// actually be sent.
|
||||
enableContentSecurityPolicy: function (_enableForTests) {
|
||||
// By default, unsafe inline scripts and styles are allowed, since we expect
|
||||
// many apps will use them for analytics, etc. Unsafe eval is disallowed, and
|
||||
// the only allowable content source is the same origin or data, except for
|
||||
// connect which allows anything (since meteor.com apps make websocket
|
||||
// connections to a lot of different origins).
|
||||
if (! _enableForTests)
|
||||
cspEnabled = true;
|
||||
else
|
||||
cspEnabledForTests = true;
|
||||
cspSrcs = {};
|
||||
BrowserPolicy.setContentSecurityPolicy("default-src 'self'; " +
|
||||
"script-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src *; " +
|
||||
"img-src data: 'self'; " +
|
||||
"style-src 'self' 'unsafe-inline';");
|
||||
},
|
||||
|
||||
setContentSecurityPolicy: function (csp) {
|
||||
throwIfNotEnabled();
|
||||
parseCsp(csp);
|
||||
},
|
||||
|
||||
// Helpers for creating content security policies
|
||||
|
||||
_keywordAllowed: function (directive, keyword, _calledFromTests) {
|
||||
// All keywords are allowed if csp is not enabled and we're not in a test
|
||||
// run. If csp is enabled or we're in a test run, then look in cspSrcs to
|
||||
// see if it's allowed.
|
||||
return (! cspEnabled && ! _calledFromTests) ||
|
||||
(cspSrcs[directive] &&
|
||||
_.indexOf(cspSrcs[directive], keyword) !== -1);
|
||||
},
|
||||
|
||||
// Used by webapp to determine whether we need an extra round trip for
|
||||
// __meteor_runtime_config__.
|
||||
// _calledFromTests is used to indicate that we should ignore cspEnabled and
|
||||
// instead look directly in cspSrcs to determine if the keyword is allowed.
|
||||
// XXX maybe this test interface could be cleaned up
|
||||
inlineScriptsAllowed: function (_calledFromTests) {
|
||||
return BrowserPolicy._keywordAllowed("script-src",
|
||||
unsafeInline, _calledFromTests);
|
||||
},
|
||||
|
||||
allowInlineScripts: function () {
|
||||
ensureDirective("script-src");
|
||||
cspSrcs["script-src"].push(unsafeInline);
|
||||
},
|
||||
disallowInlineScripts: function () {
|
||||
ensureDirective("script-src");
|
||||
removeCspSrc("script-src", unsafeInline);
|
||||
},
|
||||
allowEval: function () {
|
||||
ensureDirective("script-src");
|
||||
cspSrcs["script-src"].push(unsafeEval);
|
||||
},
|
||||
disallowEval: function () {
|
||||
ensureDirective("script-src");
|
||||
removeCspSrc("script-src", unsafeEval);
|
||||
},
|
||||
allowInlineStyles: function () {
|
||||
ensureDirective("style-src");
|
||||
cspSrcs["style-src"].push(unsafeInline);
|
||||
},
|
||||
disallowInlineStyles: function () {
|
||||
ensureDirective("style-src");
|
||||
removeCspSrc("style-src", unsafeInline);
|
||||
},
|
||||
|
||||
// Functions for setting defaults
|
||||
allowAllContentSameOrigin: function () {
|
||||
BrowserPolicy.allowAllContentOrigin(selfKeyword);
|
||||
},
|
||||
allowAllContentDataUrl: function () {
|
||||
BrowserPolicy.allowAllContentOrigin("data:");
|
||||
},
|
||||
allowAllContentOrigin: function (origin) {
|
||||
ensureDirective("default-src");
|
||||
_.each(_.keys(cspSrcs), function (directive) {
|
||||
cspSrcs[directive].push(origin);
|
||||
});
|
||||
},
|
||||
disallowAllContent: function () {
|
||||
throwIfNotEnabled();
|
||||
cspSrcs = {
|
||||
"default-src": []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// allow<Resource>Origin, allow<Resource>Data, allow<Resource>self, and
|
||||
// disallow<Resource> methods for each type of resource.
|
||||
_.each(["script", "object", "img", "media",
|
||||
"font", "connect", "style"],
|
||||
function (resource) {
|
||||
var directive = resource + "-src";
|
||||
var methodResource;
|
||||
if (resource !== "img") {
|
||||
methodResource = resource.charAt(0).toUpperCase() +
|
||||
resource.slice(1);
|
||||
} else {
|
||||
methodResource = "Image";
|
||||
}
|
||||
var allowMethodName = "allow" + methodResource + "Origin";
|
||||
var disallowMethodName = "disallow" + methodResource;
|
||||
var allowDataMethodName = "allow" + methodResource + "DataUrl";
|
||||
var allowSelfMethodName = "allow" + methodResource + "SameOrigin";
|
||||
|
||||
BrowserPolicy[allowMethodName] = function (src) {
|
||||
ensureDirective(directive);
|
||||
cspSrcs[directive].push(src);
|
||||
};
|
||||
BrowserPolicy[disallowMethodName] = function () {
|
||||
throwIfNotEnabled();
|
||||
cspSrcs[directive] = [];
|
||||
};
|
||||
BrowserPolicy[allowDataMethodName] = function () {
|
||||
ensureDirective(directive);
|
||||
cspSrcs[directive].push("data:");
|
||||
};
|
||||
BrowserPolicy[allowSelfMethodName] = function () {
|
||||
ensureDirective(directive);
|
||||
cspSrcs[directive].push(selfKeyword);
|
||||
};
|
||||
});
|
||||
14
packages/browser-policy/package.js
Normal file
14
packages/browser-policy/package.js
Normal file
@@ -0,0 +1,14 @@
|
||||
Package.describe({
|
||||
summary: "Configure security policies enforced by the browser"
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(["underscore", "webapp"], "server");
|
||||
api.add_files("browser-policy.js", "server");
|
||||
api.export("BrowserPolicy", "server");
|
||||
});
|
||||
|
||||
Package.on_test(function (api) {
|
||||
api.use(["tinytest", "browser-policy", "ejson"]);
|
||||
api.add_files("browser-policy-test.js", "server");
|
||||
});
|
||||
@@ -44,6 +44,10 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback)
|
||||
'&access_type=' + accessType +
|
||||
'&approval_prompt=' + approvalPrompt;
|
||||
|
||||
if (Accounts._options.restrictCreationByEmail) {
|
||||
loginUrl += '&hd=' + encodeURIComponent(Accounts._options.restrictCreationByEmail);
|
||||
}
|
||||
|
||||
Oauth.initiateLogin(credentialToken,
|
||||
loginUrl,
|
||||
credentialRequestCompleteCallback,
|
||||
|
||||
@@ -1018,7 +1018,7 @@ Server = function () {
|
||||
} catch (e) {
|
||||
// XXX print stack nicely
|
||||
Meteor._debug("Internal exception while processing message", msg,
|
||||
e.stack);
|
||||
e.message, e.stack);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Package.describe({
|
||||
});
|
||||
|
||||
Package.on_use(function (api) {
|
||||
api.use(['livedata', 'underscore', 'spark', 'templating'], 'client');
|
||||
api.use(['livedata', 'mongo-livedata', 'underscore', 'spark', 'templating'], 'client');
|
||||
|
||||
api.add_files([
|
||||
'madewith.css',
|
||||
|
||||
@@ -140,9 +140,8 @@ LocalCollection.prototype.findOne = function (selector, options) {
|
||||
return this.find(selector, options).fetch()[0];
|
||||
};
|
||||
|
||||
LocalCollection.Cursor.prototype.forEach = function (callback) {
|
||||
LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) {
|
||||
var self = this;
|
||||
var doc;
|
||||
|
||||
if (self.db_objects === null)
|
||||
self.db_objects = self._getRawObjects(true);
|
||||
@@ -155,12 +154,13 @@ LocalCollection.Cursor.prototype.forEach = function (callback) {
|
||||
movedBefore: true});
|
||||
|
||||
while (self.cursor_pos < self.db_objects.length) {
|
||||
var elt = EJSON.clone(self.db_objects[self.cursor_pos++]);
|
||||
var elt = EJSON.clone(self.db_objects[self.cursor_pos]);
|
||||
if (self.projection_f)
|
||||
elt = self.projection_f(elt);
|
||||
if (self._transform)
|
||||
elt = self._transform(elt);
|
||||
callback(elt);
|
||||
callback.call(thisArg, elt, self.cursor_pos, self);
|
||||
++self.cursor_pos;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -169,11 +169,11 @@ LocalCollection.Cursor.prototype.getTransform = function () {
|
||||
return self._transform;
|
||||
};
|
||||
|
||||
LocalCollection.Cursor.prototype.map = function (callback) {
|
||||
LocalCollection.Cursor.prototype.map = function (callback, thisArg) {
|
||||
var self = this;
|
||||
var res = [];
|
||||
self.forEach(function (doc) {
|
||||
res.push(callback(doc));
|
||||
self.forEach(function (doc, index) {
|
||||
res.push(callback.call(thisArg, doc, index, self));
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -184,16 +184,25 @@ Tinytest.add("minimongo - cursors", function (test) {
|
||||
|
||||
// forEach
|
||||
var count = 0;
|
||||
q.forEach(function (obj) {
|
||||
var context = {};
|
||||
q.forEach(function (obj, i, cursor) {
|
||||
test.equal(obj.i, count++);
|
||||
});
|
||||
test.equal(obj.i, i);
|
||||
test.isTrue(context === this);
|
||||
test.isTrue(cursor === q);
|
||||
}, context);
|
||||
test.equal(count, 20);
|
||||
// everything empty
|
||||
test.length(q.fetch(), 0);
|
||||
q.rewind();
|
||||
|
||||
// map
|
||||
res = q.map(function (obj) { return obj.i * 2; });
|
||||
res = q.map(function (obj, i, cursor) {
|
||||
test.equal(obj.i, i);
|
||||
test.isTrue(context === this);
|
||||
test.isTrue(cursor === q);
|
||||
return obj.i * 2;
|
||||
}, context);
|
||||
test.length(res, 20);
|
||||
for (var i = 0; i < 20; i++)
|
||||
test.equal(res[i], i * 2);
|
||||
@@ -431,6 +440,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
nomatch({a: {$ne: 1}}, {a: [1, 2]});
|
||||
nomatch({a: {$ne: 2}}, {a: [1, 2]});
|
||||
match({a: {$ne: 3}}, {a: [1, 2]});
|
||||
nomatch({'a.b': {$ne: 1}}, {a: [{b: 1}, {b: 2}]});
|
||||
nomatch({'a.b': {$ne: 2}}, {a: [{b: 1}, {b: 2}]});
|
||||
match({'a.b': {$ne: 3}}, {a: [{b: 1}, {b: 2}]});
|
||||
|
||||
nomatch({a: {$ne: {x: 1}}}, {a: {x: 1}});
|
||||
match({a: {$ne: {x: 1}}}, {a: {x: 2}});
|
||||
@@ -460,7 +472,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
nomatch({a: {$nin: [1, 2, 3]}}, {a: [2]}); // tested against mongodb
|
||||
nomatch({a: {$nin: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]});
|
||||
nomatch({a: {$nin: [1, 2, 3]}}, {a: [4, 2]});
|
||||
nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}, {b:2}]});
|
||||
match({a: {$nin: [1, 2, 3]}}, {a: [4]});
|
||||
match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}]});
|
||||
|
||||
// $size
|
||||
match({a: {$size: 0}}, {a: []});
|
||||
@@ -568,7 +582,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) {
|
||||
match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 6});
|
||||
|
||||
match({x: {$not: {$gt: 7}}}, {x: [2, 3, 4]});
|
||||
match({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}]});
|
||||
nomatch({x: {$not: {$gt: 7}}}, {x: [2, 3, 4, 10]});
|
||||
nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}, {y:10}]});
|
||||
|
||||
match({x: {$not: /a/}}, {x: "dog"});
|
||||
nomatch({x: {$not: /a/}}, {x: "cat"});
|
||||
|
||||
@@ -551,9 +551,25 @@ var compileDocumentSelector = function (docSelector) {
|
||||
perKeySelectors.push(function (doc) {
|
||||
var branchValues = lookUpByIndex(doc);
|
||||
// We apply the selector to each "branched" value and return true if any
|
||||
// match. This isn't 100% consistent with MongoDB; eg, see:
|
||||
// https://jira.mongodb.org/browse/SERVER-8585
|
||||
return _.any(branchValues, valueSelectorFunc);
|
||||
// match. However, for "negative" selectors like $ne or $not we actually
|
||||
// require *all* elements to match.
|
||||
//
|
||||
// This is because {'x.tag': {$ne: "foo"}} applied to {x: [{tag: 'foo'},
|
||||
// {tag: 'bar'}]} should NOT match even though there is a branch that
|
||||
// matches. (This matches the fact that $ne uses a negated
|
||||
// _anyIfArrayPlus, for when the last level of the key is the array,
|
||||
// which deMorgans into an 'all'.)
|
||||
//
|
||||
// XXX This isn't 100% consistent with MongoDB in 'null' cases:
|
||||
// https://jira.mongodb.org/browse/SERVER-8585
|
||||
// XXX this still isn't right. consider {a: {$ne: 5, $gt: 6}}. the
|
||||
// $ne needs to use the "all" logic and the $gt needs the "any"
|
||||
// logic
|
||||
var combiner = (subSelector &&
|
||||
(subSelector.$not || subSelector.$ne ||
|
||||
subSelector.$nin))
|
||||
? _.all : _.any;
|
||||
return combiner(branchValues, valueSelectorFunc);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -607,9 +607,15 @@ _.each(['forEach', 'map', 'rewind', 'fetch', 'count'], function (method) {
|
||||
if (self._cursorDescription.options.tailable)
|
||||
throw new Error("Cannot call " + method + " on a tailable cursor");
|
||||
|
||||
if (!self._synchronousCursor)
|
||||
if (!self._synchronousCursor) {
|
||||
self._synchronousCursor = self._mongo._createSynchronousCursor(
|
||||
self._cursorDescription, true);
|
||||
self._cursorDescription, {
|
||||
// Make sure that the "self" argument to forEach/map callbacks is the
|
||||
// Cursor, not the SynchronousCursor.
|
||||
selfForIteration: self,
|
||||
useTransform: true
|
||||
});
|
||||
}
|
||||
|
||||
return self._synchronousCursor[method].apply(
|
||||
self._synchronousCursor, arguments);
|
||||
@@ -651,20 +657,21 @@ Cursor.prototype.observeChanges = function (callbacks) {
|
||||
self._cursorDescription, ordered, callbacks);
|
||||
};
|
||||
|
||||
MongoConnection.prototype._createSynchronousCursor = function(cursorDescription,
|
||||
useTransform) {
|
||||
MongoConnection.prototype._createSynchronousCursor = function(
|
||||
cursorDescription, options) {
|
||||
var self = this;
|
||||
options = _.pick(options || {}, 'selfForIteration', 'useTransform');
|
||||
|
||||
var collection = self._getCollection(cursorDescription.collectionName);
|
||||
var options = cursorDescription.options;
|
||||
var cursorOptions = cursorDescription.options;
|
||||
var mongoOptions = {
|
||||
sort: options.sort,
|
||||
limit: options.limit,
|
||||
skip: options.skip
|
||||
sort: cursorOptions.sort,
|
||||
limit: cursorOptions.limit,
|
||||
skip: cursorOptions.skip
|
||||
};
|
||||
|
||||
// Do we want a tailable cursor (which only works on capped collections)?
|
||||
if (options.tailable) {
|
||||
if (cursorOptions.tailable) {
|
||||
// We want a tailable cursor...
|
||||
mongoOptions.tailable = true;
|
||||
// ... and for the server to wait a bit if any getMore has no data (rather
|
||||
@@ -677,16 +684,21 @@ MongoConnection.prototype._createSynchronousCursor = function(cursorDescription,
|
||||
|
||||
var dbCursor = collection.find(
|
||||
replaceTypes(cursorDescription.selector, replaceMeteorAtomWithMongo),
|
||||
options.fields, mongoOptions);
|
||||
cursorOptions.fields, mongoOptions);
|
||||
|
||||
return new SynchronousCursor(dbCursor, cursorDescription, useTransform);
|
||||
return new SynchronousCursor(dbCursor, cursorDescription, options);
|
||||
};
|
||||
|
||||
var SynchronousCursor = function (dbCursor, cursorDescription, useTransform) {
|
||||
var SynchronousCursor = function (dbCursor, cursorDescription, options) {
|
||||
var self = this;
|
||||
options = _.pick(options || {}, 'selfForIteration', 'useTransform');
|
||||
|
||||
self._dbCursor = dbCursor;
|
||||
self._cursorDescription = cursorDescription;
|
||||
if (useTransform && cursorDescription.options.transform) {
|
||||
// The "self" argument passed to forEach/map callbacks. If we're wrapped
|
||||
// inside a user-visible Cursor, we want to provide the outer cursor!
|
||||
self._selfForIteration = options.selfForIteration || self;
|
||||
if (options.useTransform && cursorDescription.options.transform) {
|
||||
self._transform = Deps._makeNonreactive(
|
||||
cursorDescription.options.transform
|
||||
);
|
||||
@@ -728,29 +740,26 @@ _.extend(SynchronousCursor.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
// XXX Make more like ECMA forEach:
|
||||
// https://github.com/meteor/meteor/pull/63#issuecomment-5320050
|
||||
forEach: function (callback) {
|
||||
forEach: function (callback, thisArg) {
|
||||
var self = this;
|
||||
|
||||
// We implement the loop ourself instead of using self._dbCursor.each,
|
||||
// because "each" will call its callback outside of a fiber which makes it
|
||||
// much more complex to make this function synchronous.
|
||||
var index = 0;
|
||||
while (true) {
|
||||
var doc = self._nextObject();
|
||||
if (!doc) return;
|
||||
callback(doc);
|
||||
callback.call(thisArg, doc, index++, self._selfForIteration);
|
||||
}
|
||||
},
|
||||
|
||||
// XXX Make more like ECMA map:
|
||||
// https://github.com/meteor/meteor/pull/63#issuecomment-5320050
|
||||
// XXX Allow overlapping callback executions if callback yields.
|
||||
map: function (callback) {
|
||||
map: function (callback, thisArg) {
|
||||
var self = this;
|
||||
var res = [];
|
||||
self.forEach(function (doc) {
|
||||
res.push(callback(doc));
|
||||
self.forEach(function (doc, index) {
|
||||
res.push(callback.call(thisArg, doc, index, self._selfForIteration));
|
||||
});
|
||||
return res;
|
||||
},
|
||||
@@ -1062,7 +1071,7 @@ _.extend(LiveResultsSet.prototype, {
|
||||
self._synchronousCursor.rewind();
|
||||
} else {
|
||||
self._synchronousCursor = self._mongoHandle._createSynchronousCursor(
|
||||
self._cursorDescription, false /* !useTransform */);
|
||||
self._cursorDescription);
|
||||
}
|
||||
var newResults = self._synchronousCursor.getRawObjects(self._ordered);
|
||||
var oldResults = self._results;
|
||||
@@ -1190,8 +1199,7 @@ MongoConnection.prototype._observeChangesTailable = function (
|
||||
+ " tailable cursor without a "
|
||||
+ (ordered ? "addedBefore" : "added") + " callback");
|
||||
}
|
||||
var cursor = self._createSynchronousCursor(cursorDescription,
|
||||
false /* useTransform */);
|
||||
var cursor = self._createSynchronousCursor(cursorDescription);
|
||||
|
||||
var stopped = false;
|
||||
var lastTS = undefined;
|
||||
@@ -1233,7 +1241,7 @@ MongoConnection.prototype._observeChangesTailable = function (
|
||||
cursor = self._createSynchronousCursor(new CursorDescription(
|
||||
cursorDescription.collectionName,
|
||||
newSelector,
|
||||
cursorDescription.options), false /* useTransform */);
|
||||
cursorDescription.options));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,7 +175,12 @@ Tinytest.addAsync("mongo-livedata - basics, " + idGeneration, function (test, on
|
||||
|
||||
var cur = coll.find({run: run}, {sort: ["x"]});
|
||||
var total = 0;
|
||||
cur.forEach(function (doc) {
|
||||
var index = 0;
|
||||
var context = {};
|
||||
cur.forEach(function (doc, i, cursor) {
|
||||
test.equal(i, index++);
|
||||
test.isTrue(cursor === cur);
|
||||
test.isTrue(context === this);
|
||||
total *= 10;
|
||||
if (Meteor.isServer) {
|
||||
// Verify that the callbacks from forEach run sequentially and that
|
||||
@@ -189,13 +194,19 @@ Tinytest.addAsync("mongo-livedata - basics, " + idGeneration, function (test, on
|
||||
total += doc.x;
|
||||
// verify the meteor environment is set up here
|
||||
coll2.insert({total:total});
|
||||
});
|
||||
}, context);
|
||||
test.equal(total, 14);
|
||||
|
||||
cur.rewind();
|
||||
test.equal(cur.map(function (doc) {
|
||||
index = 0;
|
||||
test.equal(cur.map(function (doc, i, cursor) {
|
||||
// XXX we could theoretically make map run its iterations in parallel or
|
||||
// something which would make this fail
|
||||
test.equal(i, index++);
|
||||
test.isTrue(cursor === cur);
|
||||
test.isTrue(context === this);
|
||||
return doc.x * 2;
|
||||
}), [2, 8]);
|
||||
}, context), [2, 8]);
|
||||
|
||||
test.equal(_.pluck(coll.find({run: run}, {sort: {x: -1}}).fetch(), "x"),
|
||||
[4, 1]);
|
||||
|
||||
@@ -4,16 +4,16 @@ var querystring = Npm.require("querystring");
|
||||
// An OAuth1 wrapper around http calls which helps get tokens and
|
||||
// takes care of HTTP headers
|
||||
//
|
||||
// @param consumerKey {String} As supplied by the OAuth1 provider
|
||||
// @param consumerSecret {String} As supplied by the OAuth1 provider
|
||||
// @param config {Object}
|
||||
// - consumerKey (String): oauth consumer key
|
||||
// - secret (String): oauth consumer secret
|
||||
// @param urls {Object}
|
||||
// - requestToken (String): url
|
||||
// - authorize (String): url
|
||||
// - accessToken (String): url
|
||||
// - authenticate (String): url
|
||||
OAuth1Binding = function(consumerKey, consumerSecret, urls) {
|
||||
this._consumerKey = consumerKey;
|
||||
this._secret = consumerSecret;
|
||||
OAuth1Binding = function(config, urls) {
|
||||
this._config = config;
|
||||
this._urls = urls;
|
||||
};
|
||||
|
||||
@@ -27,15 +27,26 @@ OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) {
|
||||
var response = self._call('POST', self._urls.requestToken, headers);
|
||||
var tokens = querystring.parse(response.content);
|
||||
|
||||
// XXX should we also store oauth_token_secret here?
|
||||
if (!tokens.oauth_callback_confirmed)
|
||||
throw new Error("oauth_callback_confirmed false when requesting oauth1 token", tokens);
|
||||
throw new Error(
|
||||
"oauth_callback_confirmed false when requesting oauth1 token", tokens);
|
||||
|
||||
self.requestToken = tokens.oauth_token;
|
||||
self.requestTokenSecret = tokens.oauth_token_secret;
|
||||
};
|
||||
|
||||
OAuth1Binding.prototype.prepareAccessToken = function(query) {
|
||||
OAuth1Binding.prototype.prepareAccessToken = function(query, requestTokenSecret) {
|
||||
var self = this;
|
||||
|
||||
// support implementations that use request token secrets. This is
|
||||
// read by self._call.
|
||||
//
|
||||
// XXX make it a param to call, not something stashed on self? It's
|
||||
// kinda confusing right now, everything except this is passed as
|
||||
// arguments, but this is stored.
|
||||
if (requestTokenSecret)
|
||||
self.accessTokenSecret = requestTokenSecret;
|
||||
|
||||
var headers = self._buildHeader({
|
||||
oauth_token: query.oauth_token
|
||||
});
|
||||
@@ -76,7 +87,7 @@ OAuth1Binding.prototype.post = function(url, params, callback) {
|
||||
OAuth1Binding.prototype._buildHeader = function(headers) {
|
||||
var self = this;
|
||||
return _.extend({
|
||||
oauth_consumer_key: self._consumerKey,
|
||||
oauth_consumer_key: self._config.consumerKey,
|
||||
oauth_nonce: Random.id().replace(/\W/g, ''),
|
||||
oauth_signature_method: 'HMAC-SHA1',
|
||||
oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(),
|
||||
@@ -98,7 +109,7 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access
|
||||
self._encodeString(parameters)
|
||||
].join('&');
|
||||
|
||||
var signingKey = self._encodeString(self._secret) + '&';
|
||||
var signingKey = self._encodeString(self._config.secret) + '&';
|
||||
if (accessTokenSecret)
|
||||
signingKey += self._encodeString(accessTokenSecret);
|
||||
|
||||
@@ -108,8 +119,14 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access
|
||||
OAuth1Binding.prototype._call = function(method, url, headers, params, callback) {
|
||||
var self = this;
|
||||
|
||||
// all URLs to be functions to support parameters/customization
|
||||
if(typeof url === "function") {
|
||||
url = url(self);
|
||||
}
|
||||
|
||||
// Get the signature
|
||||
headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret, params);
|
||||
headers.oauth_signature =
|
||||
self._getSignature(method, url, headers, self.accessTokenSecret, params);
|
||||
|
||||
// Make a authorization string according to oauth1 spec
|
||||
var authString = self._getAuthHeaderString(headers);
|
||||
|
||||
@@ -12,8 +12,7 @@ Oauth._requestHandlers['1'] = function (service, query, res) {
|
||||
}
|
||||
|
||||
var urls = service.urls;
|
||||
var oauthBinding = new OAuth1Binding(
|
||||
config.consumerKey, config.secret, urls);
|
||||
var oauthBinding = new OAuth1Binding(config, urls);
|
||||
|
||||
if (query.requestTokenAndRedirect) {
|
||||
// step 1 - get and store a request token
|
||||
@@ -22,10 +21,20 @@ Oauth._requestHandlers['1'] = function (service, query, res) {
|
||||
oauthBinding.prepareRequestToken(query.requestTokenAndRedirect);
|
||||
|
||||
// Keep track of request token so we can verify it on the next step
|
||||
requestTokens[query.state] = oauthBinding.requestToken;
|
||||
requestTokens[query.state] = {
|
||||
requestToken: oauthBinding.requestToken,
|
||||
requestTokenSecret: oauthBinding.requestTokenSecret
|
||||
};
|
||||
|
||||
// support for scope/name parameters
|
||||
var redirectUrl = undefined;
|
||||
if(typeof urls.authenticate === "function") {
|
||||
redirectUrl = urls.authenticate(oauthBinding);
|
||||
} else {
|
||||
redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken;
|
||||
}
|
||||
|
||||
// redirect to provider login, which will redirect back to "step 2" below
|
||||
var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken;
|
||||
res.writeHead(302, {'Location': redirectUrl});
|
||||
res.end();
|
||||
} else {
|
||||
@@ -34,7 +43,8 @@ Oauth._requestHandlers['1'] = function (service, query, res) {
|
||||
// token and access token secret and log in as user
|
||||
|
||||
// Get the user's request token so we can verify it and clear it
|
||||
var requestToken = requestTokens[query.state];
|
||||
var requestToken = requestTokens[query.state].requestToken;
|
||||
var requestTokenSecret = requestTokens[query.state].requestTokenSecret;
|
||||
delete requestTokens[query.state];
|
||||
|
||||
// Verify user authorized access and the oauth_token matches
|
||||
@@ -45,17 +55,17 @@ Oauth._requestHandlers['1'] = function (service, query, res) {
|
||||
// subsequent call to the `login` method will be immediate.
|
||||
|
||||
// Get the access token for signing requests
|
||||
oauthBinding.prepareAccessToken(query);
|
||||
oauthBinding.prepareAccessToken(query, requestTokenSecret);
|
||||
|
||||
// Run service-specific handler.
|
||||
var oauthResult = service.handleOauthRequest(oauthBinding);
|
||||
|
||||
// Add the login result to the result map
|
||||
Oauth._loginResultForCredentialToken[query.state] = {
|
||||
serviceName: service.serviceName,
|
||||
serviceData: oauthResult.serviceData,
|
||||
options: oauthResult.options
|
||||
};
|
||||
serviceName: service.serviceName,
|
||||
serviceData: oauthResult.serviceData,
|
||||
options: oauthResult.options
|
||||
};
|
||||
}
|
||||
|
||||
// Either close the window, redirect, or render nothing
|
||||
|
||||
@@ -40,7 +40,9 @@ Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test)
|
||||
});
|
||||
|
||||
// simulate logging in using twitterfoo
|
||||
OAuth1Test.requestTokens[credentialToken] = twitterfooAccessToken;
|
||||
OAuth1Test.requestTokens[credentialToken] = {
|
||||
requestToken: twitterfooAccessToken
|
||||
};
|
||||
|
||||
var req = {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
// We use cryptographically strong PRNGs (crypto.getRandomBytes() on the server,
|
||||
// window.crypto.getRandomValues() in the browser) when available. If these
|
||||
// PRNGs fail, we fall back to the Alea PRNG, which is not cryptographically
|
||||
// strong, and we seed it with various sources such as the date, Math.random,
|
||||
// and window size on the client. When using crypto.getRandomValues(), our
|
||||
// primitive is hexString(), from which we construct fraction(). When using
|
||||
// window.crypto.getRandomValues() or alea, the primitive is fraction and we use
|
||||
// that to construct hex string.
|
||||
|
||||
if (Meteor.isServer)
|
||||
var nodeCrypto = Npm.require('crypto');
|
||||
|
||||
// see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript
|
||||
// for a full discussion and Alea implementation.
|
||||
var Alea = function () {
|
||||
@@ -75,52 +87,79 @@ var Alea = function () {
|
||||
|
||||
var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
|
||||
var create = function (/* arguments */) {
|
||||
|
||||
var random = Alea.apply(null, arguments);
|
||||
|
||||
var self = {};
|
||||
|
||||
var bind = function (fn) {
|
||||
return _.bind(fn, self);
|
||||
};
|
||||
|
||||
return _.extend(self, {
|
||||
_Alea: Alea,
|
||||
|
||||
create: create,
|
||||
|
||||
fraction: random,
|
||||
|
||||
choice: bind(function (arrayOrString) {
|
||||
var index = Math.floor(this.fraction() * arrayOrString.length);
|
||||
if (typeof arrayOrString === "string")
|
||||
return arrayOrString.substr(index, 1);
|
||||
else
|
||||
return arrayOrString[index];
|
||||
}),
|
||||
|
||||
id: bind(function() {
|
||||
var digits = [];
|
||||
// Length of 17 preserves around 96 bits of entropy, which is the
|
||||
// amount of state in our PRNG
|
||||
for (var i = 0; i < 17; i++) {
|
||||
digits[i] = this.choice(UNMISTAKABLE_CHARS);
|
||||
}
|
||||
return digits.join("");
|
||||
}),
|
||||
|
||||
hexString: bind(function (digits) {
|
||||
var hexDigits = [];
|
||||
for (var i = 0; i < digits; ++i) {
|
||||
hexDigits.push(this.choice("0123456789abcdef"));
|
||||
}
|
||||
return hexDigits.join('');
|
||||
})
|
||||
});
|
||||
// If seeds are provided, then the alea PRNG will be used, since cryptographic
|
||||
// PRNGs (Node crypto and window.crypto.getRandomValues) don't allow us to
|
||||
// specify seeds. The caller is responsible for making sure to provide a seed
|
||||
// for alea if a csprng is not available.
|
||||
var RandomGenerator = function (seedArray) {
|
||||
var self = this;
|
||||
if (seedArray !== undefined)
|
||||
self.alea = Alea.apply(null, seedArray);
|
||||
self._Alea = Alea;
|
||||
};
|
||||
|
||||
// instantiate RNG. Heuristically collect entropy from various sources
|
||||
RandomGenerator.prototype.fraction = function () {
|
||||
var self = this;
|
||||
if (self.alea) {
|
||||
return self.alea();
|
||||
} else if (nodeCrypto) {
|
||||
var numerator = parseInt(self.hexString(8), 16);
|
||||
return numerator * 2.3283064365386963e-10; // 2^-32
|
||||
} else if (typeof window !== "undefined" && window.crypto &&
|
||||
window.crypto.getRandomValues) {
|
||||
var array = new Uint32Array(1);
|
||||
window.crypto.getRandomValues(array);
|
||||
return array[0] * 2.3283064365386963e-10; // 2^-32
|
||||
}
|
||||
};
|
||||
|
||||
RandomGenerator.prototype.hexString = function (digits) {
|
||||
var self = this;
|
||||
if (nodeCrypto && ! self.alea) {
|
||||
var numBytes = Math.ceil(digits / 2);
|
||||
var bytes;
|
||||
// Try to get cryptographically strong randomness. Fall back to
|
||||
// non-cryptographically strong if not available.
|
||||
try {
|
||||
bytes = nodeCrypto.randomBytes(numBytes);
|
||||
} catch (e) {
|
||||
// XXX should re-throw any error except insufficient entropy
|
||||
bytes = nodeCrypto.pseudoRandomBytes(numBytes);
|
||||
}
|
||||
var result = bytes.toString("hex");
|
||||
// If the number of digits is odd, we'll have generated an extra 4 bits
|
||||
// of randomness, so we need to trim the last digit.
|
||||
return result.substring(0, digits);
|
||||
} else {
|
||||
var hexDigits = [];
|
||||
for (var i = 0; i < digits; ++i) {
|
||||
hexDigits.push(self.choice("0123456789abcdef"));
|
||||
}
|
||||
return hexDigits.join('');
|
||||
}
|
||||
};
|
||||
|
||||
RandomGenerator.prototype.id = function () {
|
||||
var digits = [];
|
||||
var self = this;
|
||||
// Length of 17 preserves around 96 bits of entropy, which is the
|
||||
// amount of state in the Alea PRNG.
|
||||
for (var i = 0; i < 17; i++) {
|
||||
digits[i] = self.choice(UNMISTAKABLE_CHARS);
|
||||
}
|
||||
return digits.join("");
|
||||
};
|
||||
|
||||
RandomGenerator.prototype.choice = function (arrayOrString) {
|
||||
var index = Math.floor(this.fraction() * arrayOrString.length);
|
||||
if (typeof arrayOrString === "string")
|
||||
return arrayOrString.substr(index, 1);
|
||||
else
|
||||
return arrayOrString[index];
|
||||
};
|
||||
|
||||
// instantiate RNG. Heuristically collect entropy from various sources when a
|
||||
// cryptographic PRNG isn't available.
|
||||
|
||||
// client sources
|
||||
var height = (typeof window !== 'undefined' && window.innerHeight) ||
|
||||
@@ -143,12 +182,13 @@ var width = (typeof window !== 'undefined' && window.innerWidth) ||
|
||||
|
||||
var agent = (typeof navigator !== 'undefined' && navigator.userAgent) || "";
|
||||
|
||||
// server sources
|
||||
var pid = (typeof process !== 'undefined' && process.pid) || 1;
|
||||
if (nodeCrypto ||
|
||||
(typeof window !== "undefined" &&
|
||||
window.crypto && window.crypto.getRandomValues))
|
||||
Random = new RandomGenerator();
|
||||
else
|
||||
Random = new RandomGenerator([new Date(), height, width, agent, Math.random()]);
|
||||
|
||||
// XXX On the server, use the crypto module (OpenSSL) instead of this PRNG.
|
||||
// (Make Random.fraction be generated from Random.hexString instead of the
|
||||
// other way around, and generate Random.hexString from crypto.randomBytes.)
|
||||
Random = create([
|
||||
new Date(), height, width, agent, pid, Math.random()
|
||||
]);
|
||||
Random.create = function () {
|
||||
return new RandomGenerator(arguments);
|
||||
};
|
||||
|
||||
@@ -13,3 +13,17 @@ Tinytest.add('random', function (test) {
|
||||
test.equal(random.id(), "shxDnjWWmnKPEoLhM");
|
||||
test.equal(random.id(), "6QTjB8C5SEqhmz4ni");
|
||||
});
|
||||
|
||||
// node crypto and window.crypto.getRandomValues() don't let us specify a seed,
|
||||
// but at least test that the output is in the right format.
|
||||
Tinytest.add('random - format', function (test) {
|
||||
var idLen = 17;
|
||||
test.equal(Random.id().length, idLen);
|
||||
var numDigits = 9;
|
||||
var hexStr = Random.hexString(numDigits);
|
||||
test.equal(hexStr.length, numDigits);
|
||||
parseInt(hexStr, 16); // should not throw
|
||||
var frac = Random.fraction();
|
||||
test.isTrue(frac < 1.0);
|
||||
test.isTrue(frac >= 0.0);
|
||||
});
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
* the client's session to render properly.
|
||||
*/
|
||||
|
||||
// XXX when making this API public, also expose a flag for the app
|
||||
// developer to know whether a hot code push is happening. This is
|
||||
// useful for apps using `window.onbeforeunload`. See
|
||||
// https://github.com/meteor/meteor/pull/657
|
||||
|
||||
var KEY_NAME = 'Meteor_Reload';
|
||||
// after how long should we consider this no longer an automatic
|
||||
// reload, but a fresh restart. This only happens if a reload is
|
||||
|
||||
@@ -1876,7 +1876,7 @@ Tinytest.add("spark - leaderboard, " + idGeneration, function(test) {
|
||||
}));
|
||||
var idGen;
|
||||
if (idGeneration === 'STRING')
|
||||
idGen = Random.id;
|
||||
idGen = _.bind(Random.id, Random);
|
||||
else
|
||||
idGen = function () { return new LocalCollection._ObjectID(); };
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ SeededRandom = function(seed) { // seed may be a string or any type
|
||||
return new SeededRandom(seed);
|
||||
|
||||
seed = seed || "seed";
|
||||
this.gen = new Random._Alea(seed); // from random.js
|
||||
this.gen = Random.create(seed)._Alea; // from random.js
|
||||
};
|
||||
SeededRandom.prototype.next = function() {
|
||||
return this.gen();
|
||||
|
||||
@@ -12,6 +12,11 @@ Package.on_use(function (api) {
|
||||
api.use(['application-configuration'], {
|
||||
unordered: true
|
||||
});
|
||||
// At response serving time, webapp uses browser-policy if it is loaded. If
|
||||
// browser-policy is loaded, then it must be loaded after webapp
|
||||
// (browser-policy depends on webapp). So we don't explicitly depend in any
|
||||
// way on browser-policy here, but we use it when it is loaded, and it can be
|
||||
// loaded after webapp.
|
||||
api.export(['WebApp', 'main', 'WebAppInternals'], 'server');
|
||||
api.add_files('webapp_server.js', 'server');
|
||||
});
|
||||
|
||||
@@ -147,7 +147,6 @@ var appUrl = function (url) {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
var runWebAppServer = function () {
|
||||
// read the control for the client we'll be serving up
|
||||
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
|
||||
@@ -231,6 +230,17 @@ var runWebAppServer = function () {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Package["browser-policy"] &&
|
||||
! Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed() &&
|
||||
pathname === "/meteor_runtime_config.js") {
|
||||
res.writeHead(200, { 'Content-type': 'application/javascript' });
|
||||
res.write("__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";");
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_.has(staticFiles, pathname)) {
|
||||
next();
|
||||
return;
|
||||
@@ -387,15 +397,25 @@ var runWebAppServer = function () {
|
||||
argv = optimist(argv).boolean('keepalive').argv;
|
||||
|
||||
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
|
||||
boilerplateHtml =
|
||||
fs.readFileSync(boilerplateHtmlPath, 'utf8')
|
||||
.replace(
|
||||
"// ##RUNTIME_CONFIG##",
|
||||
"__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";")
|
||||
.replace(
|
||||
/##ROOT_URL_PATH_PREFIX##/g,
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
||||
boilerplateHtml = fs.readFileSync(boilerplateHtmlPath, 'utf8');
|
||||
|
||||
// Include __meteor_runtime_config__ in the app html, as an inline script if
|
||||
// it's not forbidden by CSP.
|
||||
if (! Package["browser-policy"] ||
|
||||
Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed()) {
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RUNTIME_CONFIG##/,
|
||||
"<script type='text/javascript'>__meteor_runtime_config__ = " +
|
||||
JSON.stringify(__meteor_runtime_config__) + ";</script>");
|
||||
} else {
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##RUNTIME_CONFIG##/,
|
||||
"<script type='text/javascript' src='##ROOT_URL_PATH_PREFIX##/meteor_runtime_config.js'></script>"
|
||||
);
|
||||
}
|
||||
boilerplateHtml = boilerplateHtml.replace(
|
||||
/##ROOT_URL_PATH_PREFIX##/g,
|
||||
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
|
||||
|
||||
// only start listening after all the startup code has run.
|
||||
var localPort = parseInt(process.env.PORT) || 0;
|
||||
|
||||
@@ -74,9 +74,9 @@ cd build
|
||||
git clone git://github.com/joyent/node.git
|
||||
cd node
|
||||
# When upgrading node versions, also update the values of MIN_NODE_VERSION at
|
||||
# the top of tools/meteor.js and tools/server/server.js, and the text in
|
||||
# the top of tools/meteor.js and tools/server/boot.js, and the text in
|
||||
# docs/client/concepts.html and the README in tools/bundler.js.
|
||||
git checkout v0.8.24
|
||||
git checkout v0.10.19
|
||||
|
||||
./configure --prefix="$DIR"
|
||||
make -j4
|
||||
@@ -102,7 +102,6 @@ npm install semver@1.1.0
|
||||
npm install handlebars@1.0.7
|
||||
npm install request@2.12.0
|
||||
npm install keypress@0.1.0
|
||||
npm install http-proxy@0.10.1 # not 0.10.2, which contains a sketchy websocket change
|
||||
npm install underscore@1.5.1
|
||||
npm install fstream@0.1.21
|
||||
npm install tar@0.1.14
|
||||
@@ -111,6 +110,11 @@ npm install shell-quote@0.0.1 # now at 1.3.3, which adds plenty of options to
|
||||
npm install byline@2.0.3 # v3 requires node 0.10
|
||||
npm install source-map@0.1.26
|
||||
|
||||
# Using the unreleased "caronte" rewrite of http-proxy (which is even called
|
||||
# 'caronte', though this may change when this eventually hopefully becomes
|
||||
# http-proxy 1.0).
|
||||
npm install https://github.com/nodejitsu/node-http-proxy/tarball/94ec6fa5ce6826ca1e8974f7e99b31541aaad76a
|
||||
|
||||
# Using the unreleased 1.1 branch. We can probably switch to a built NPM version
|
||||
# when it gets released.
|
||||
npm install https://github.com/ariya/esprima/tarball/5044b87f94fb802d9609f1426c838874ec2007b3
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
{{#each stylesheets}} <link rel="stylesheet" href="##ROOT_URL_PATH_PREFIX##{{this}}">
|
||||
{{/each}}
|
||||
|
||||
<script type="text/javascript">
|
||||
// ##RUNTIME_CONFIG##
|
||||
</script>
|
||||
##RUNTIME_CONFIG##
|
||||
|
||||
{{#each scripts}} <script type="text/javascript" src="##ROOT_URL_PATH_PREFIX##{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
@@ -1442,8 +1442,8 @@ var writeSiteArchive = function (targets, outputPath, options) {
|
||||
|
||||
builder.write('README', { data: new Buffer(
|
||||
"This is a Meteor application bundle. It has only one dependency:\n" +
|
||||
"Node.js 0.8 (with the 'fibers' package). The current release of Meteor\n" +
|
||||
"has been tested with Node 0.8.24. To run the application:\n" +
|
||||
"Node.js 0.10 (with the 'fibers' package). The current release of Meteor\n" +
|
||||
"has been tested with Node 0.10.19. To run the application:\n" +
|
||||
"\n" +
|
||||
" $ npm install fibers@1.0.1\n" +
|
||||
" $ export MONGO_URL='mongodb://user:password@host:port/databasename'\n" +
|
||||
|
||||
@@ -5,7 +5,7 @@ var fs = require('fs');
|
||||
var unipackage = require('./unipackage.js');
|
||||
var fiberHelpers = require('./fiber-helpers.js');
|
||||
var Fiber = require('fibers');
|
||||
var request = require('request');
|
||||
var httpHelpers = require('./http-helpers.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
// a bit of a hack
|
||||
@@ -90,7 +90,7 @@ exports.discoverGalaxy = function (app) {
|
||||
|
||||
// At some point we may want to send a version in the request so that galaxy
|
||||
// can respond differently to different versions of meteor.
|
||||
request({
|
||||
httpHelpers.request({
|
||||
url: url,
|
||||
json: true,
|
||||
strictSSL: true,
|
||||
@@ -210,7 +210,8 @@ exports.deploy = function (options) {
|
||||
var fileSize = fs.statSync(starball).size;
|
||||
var fileStream = fs.createReadStream(starball);
|
||||
var future = new Future;
|
||||
var req = request.put({
|
||||
var req = httpHelpers.request({
|
||||
method: "PUT",
|
||||
url: info.put,
|
||||
headers: { 'content-length': fileSize,
|
||||
'content-type': 'application/octet-stream' },
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
var qs = require('querystring');
|
||||
var path = require('path');
|
||||
var files = require('./files.js');
|
||||
var httpHelpers = require('./http-helpers.js');
|
||||
var warehouse = require('./warehouse.js');
|
||||
var buildmessage = require('./buildmessage.js');
|
||||
var _ = require('underscore');
|
||||
@@ -43,15 +44,16 @@ var meteor_rpc = function (rpc_name, method, site, query_params, callback) {
|
||||
url += '?' + qs.stringify(query_params);
|
||||
}
|
||||
|
||||
var request = require('request');
|
||||
var r = request({method: method, url: url}, function (error, response, body) {
|
||||
if (error || ((response.statusCode !== 200)
|
||||
&& (response.statusCode !== 201)))
|
||||
// pass some non-falsy error back to callback
|
||||
callback(error || response.statusCode, body);
|
||||
else
|
||||
callback(null, body);
|
||||
});
|
||||
var r = httpHelpers.request(
|
||||
{method: method, url: url},
|
||||
function (error, response, body) {
|
||||
if (error || ((response.statusCode !== 200)
|
||||
&& (response.statusCode !== 201)))
|
||||
// pass some non-falsy error back to callback
|
||||
callback(error || response.statusCode, body);
|
||||
else
|
||||
callback(null, body);
|
||||
});
|
||||
|
||||
return r;
|
||||
};
|
||||
@@ -345,8 +347,7 @@ var with_password = function (site, callback) {
|
||||
// Future.throw. Basically, what Future.wrap does.
|
||||
callback = inFiber(callback);
|
||||
|
||||
var request = require('request');
|
||||
request(check_url, function (error, response, body) {
|
||||
httpHelpers.request(check_url, function (error, response, body) {
|
||||
if (error || response.statusCode !== 200) {
|
||||
callback();
|
||||
|
||||
|
||||
@@ -436,59 +436,6 @@ _.extend(exports, {
|
||||
future.wait();
|
||||
},
|
||||
|
||||
// A synchronous wrapper around request(...) that returns the response "body"
|
||||
// or throws.
|
||||
getUrl: function (urlOrOptions, callback) {
|
||||
var future = new Future;
|
||||
// can't just use Future.wrap, because we want to return "body", not
|
||||
// "response".
|
||||
|
||||
urlOrOptions = _.clone(urlOrOptions); // we are going to change it
|
||||
var appVersion;
|
||||
try {
|
||||
appVersion = getToolsVersion();
|
||||
} catch(e) {
|
||||
appVersion = 'checkout';
|
||||
}
|
||||
|
||||
// meteorReleaseContext - an option with information about app directory
|
||||
// release versions, etc, is used to get exact Meteor version used.
|
||||
if (urlOrOptions.hasOwnProperty('meteorReleaseContext')) {
|
||||
// Get meteor app release version: if specified in command line args, take
|
||||
// releaseVersion, if not specified, try global meteor version
|
||||
var meteorReleaseContext = urlOrOptions.meteorReleaseContext;
|
||||
appVersion = meteorReleaseContext.releaseVersion;
|
||||
|
||||
if (appVersion === 'none')
|
||||
appVersion = meteorReleaseContext.appReleaseVersion;
|
||||
if (appVersion === 'none')
|
||||
appVersion = 'checkout';
|
||||
|
||||
delete urlOrOptions.meteorReleaseContext;
|
||||
}
|
||||
|
||||
// Get some kind of User Agent: environment information.
|
||||
var ua = util.format('Meteor/%s OS/%s (%s; %s; %s;)',
|
||||
appVersion, os.platform(), os.type(), os.release(), os.arch());
|
||||
|
||||
var headers = {'User-Agent': ua };
|
||||
|
||||
if (_.isObject(urlOrOptions))
|
||||
urlOrOptions.headers = _.extend(headers, urlOrOptions.headers);
|
||||
else
|
||||
urlOrOptions = { url: urlOrOptions, headers: headers };
|
||||
|
||||
var request = require('request');
|
||||
request(urlOrOptions, function (error, response, body) {
|
||||
if (error)
|
||||
future.throw(new files.OfflineError(error));
|
||||
else if (response.statusCode >= 400 && response.statusCode < 600)
|
||||
future.throw(response);
|
||||
else
|
||||
future.return(body);
|
||||
});
|
||||
return future.wait();
|
||||
},
|
||||
|
||||
// Use this if you'd like to replace a directory with another directory as
|
||||
// close to atomically as possible. It's better than recursively deleting the
|
||||
|
||||
95
tools/http-helpers.js
Normal file
95
tools/http-helpers.js
Normal file
@@ -0,0 +1,95 @@
|
||||
///
|
||||
/// utility functions for dealing with urls and http
|
||||
///
|
||||
|
||||
var os = require('os');
|
||||
var util = require('util');
|
||||
|
||||
var _ = require('underscore');
|
||||
var request = require('request');
|
||||
var Future = require('fibers/future');
|
||||
|
||||
var files = require('./files.js');
|
||||
|
||||
|
||||
var httpHelpers = exports;
|
||||
_.extend(exports, {
|
||||
|
||||
// A wrapper around request that sets http proxy.
|
||||
request: function (urlOrOptions, callback) {
|
||||
|
||||
if (!_.isObject(urlOrOptions))
|
||||
urlOrOptions = { url: urlOrOptions };
|
||||
|
||||
var url = urlOrOptions.url;
|
||||
|
||||
// try to get proxy from environment
|
||||
var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
|
||||
// if we're going to an https url, try the https_proxy env variable first.
|
||||
if (/^https/i.test(url)) {
|
||||
proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy;
|
||||
}
|
||||
if (proxy && !urlOrOptions.proxy) {
|
||||
urlOrOptions.proxy = proxy;
|
||||
}
|
||||
|
||||
return request(urlOrOptions, callback);
|
||||
},
|
||||
|
||||
|
||||
|
||||
// A synchronous wrapper around request(...) that returns the response "body"
|
||||
// or throws.
|
||||
getUrl: function (urlOrOptions, callback) {
|
||||
var future = new Future;
|
||||
// can't just use Future.wrap, because we want to return "body", not
|
||||
// "response".
|
||||
|
||||
urlOrOptions = _.clone(urlOrOptions); // we are going to change it
|
||||
var appVersion;
|
||||
try {
|
||||
appVersion = files.getToolsVersion();
|
||||
} catch(e) {
|
||||
appVersion = 'checkout';
|
||||
}
|
||||
|
||||
// meteorReleaseContext - an option with information about app directory
|
||||
// release versions, etc, is used to get exact Meteor version used.
|
||||
if (urlOrOptions.hasOwnProperty('meteorReleaseContext')) {
|
||||
// Get meteor app release version: if specified in command line args, take
|
||||
// releaseVersion, if not specified, try global meteor version
|
||||
var meteorReleaseContext = urlOrOptions.meteorReleaseContext;
|
||||
appVersion = meteorReleaseContext.releaseVersion;
|
||||
|
||||
if (appVersion === 'none')
|
||||
appVersion = meteorReleaseContext.appReleaseVersion;
|
||||
if (appVersion === 'none')
|
||||
appVersion = 'checkout';
|
||||
|
||||
delete urlOrOptions.meteorReleaseContext;
|
||||
}
|
||||
|
||||
// Get some kind of User Agent: environment information.
|
||||
var ua = util.format('Meteor/%s OS/%s (%s; %s; %s;)',
|
||||
appVersion, os.platform(), os.type(), os.release(), os.arch());
|
||||
|
||||
var headers = {'User-Agent': ua };
|
||||
|
||||
if (_.isObject(urlOrOptions))
|
||||
urlOrOptions.headers = _.extend(headers, urlOrOptions.headers);
|
||||
else
|
||||
urlOrOptions = { url: urlOrOptions, headers: headers };
|
||||
|
||||
httpHelpers.request(urlOrOptions, function (error, response, body) {
|
||||
if (error)
|
||||
future.throw(new files.OfflineError(error));
|
||||
else if (response.statusCode >= 400 && response.statusCode < 600)
|
||||
future.throw(response);
|
||||
else
|
||||
future.return(body);
|
||||
});
|
||||
return future.wait();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -24,7 +24,7 @@ Fiber(function () {
|
||||
|
||||
var Future = require('fibers/future');
|
||||
// This code is duplicated in app/server/server.js.
|
||||
var MIN_NODE_VERSION = 'v0.8.24';
|
||||
var MIN_NODE_VERSION = 'v0.10.19';
|
||||
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
|
||||
process.stderr.write(
|
||||
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');
|
||||
|
||||
@@ -10,6 +10,7 @@ var path = require('path');
|
||||
var fs = require('fs');
|
||||
var cleanup = require(path.join(__dirname, 'cleanup.js'));
|
||||
var files = require(path.join(__dirname, 'files.js'));
|
||||
var httpHelpers = require('./http-helpers.js');
|
||||
var buildmessage = require('./buildmessage.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
@@ -186,6 +187,26 @@ _.extend(exports, {
|
||||
throw new Error(
|
||||
"Corrupted .npm directory -- can't find npm-shrinkwrap.json in " + packageNpmDir);
|
||||
|
||||
// We need to rebuild all node modules when the Node version changes, in
|
||||
// case there are some binary ones. Technically this is racey, but it
|
||||
// shouldn't fail very often.
|
||||
if (fs.existsSync(path.join(packageNpmDir, 'node_modules'))) {
|
||||
var oldNodeVersion;
|
||||
try {
|
||||
oldNodeVersion = fs.readFileSync(
|
||||
path.join(packageNpmDir, 'node_modules', '.node_version'), 'utf8');
|
||||
} catch (e) {
|
||||
if (e.code !== 'ENOENT')
|
||||
throw e;
|
||||
// Use the Node version from the last release where we didn't drop this
|
||||
// file.
|
||||
oldNodeVersion = 'v0.8.24';
|
||||
}
|
||||
|
||||
if (oldNodeVersion !== process.version)
|
||||
files.rm_recursive(path.join(packageNpmDir, 'node_modules'));
|
||||
}
|
||||
|
||||
var installedDependencies = self._installedDependencies(packageNpmDir);
|
||||
|
||||
// If we already have the right things installed, life is good.
|
||||
@@ -276,6 +297,7 @@ _.extend(exports, {
|
||||
fs.unlinkSync(path.join(newPackageNpmDir, 'package.json'));
|
||||
|
||||
self._createReadme(newPackageNpmDir);
|
||||
self._createNodeVersion(newPackageNpmDir);
|
||||
files.renameDirAlmostAtomically(newPackageNpmDir, packageNpmDir);
|
||||
},
|
||||
|
||||
@@ -292,6 +314,12 @@ _.extend(exports, {
|
||||
);
|
||||
},
|
||||
|
||||
_createNodeVersion: function(newPackageNpmDir) {
|
||||
fs.writeFileSync(
|
||||
path.join(newPackageNpmDir, 'node_modules', '.node_version'),
|
||||
process.version);
|
||||
},
|
||||
|
||||
// Returns object with keys 'stdout', 'stderr', and 'success' (true
|
||||
// for clean exit with exit code 0, else false)
|
||||
_execFileSync: function(file, args, opts) {
|
||||
@@ -407,9 +435,18 @@ _.extend(exports, {
|
||||
// We don't use npm.commands.install since we couldn't
|
||||
// figure out how to silence all output (specifically the
|
||||
// installed tree which is printed out with `console.log`)
|
||||
//
|
||||
// We use --force, because the NPM cache is broken! See
|
||||
// https://github.com/isaacs/npm/issues/3265 Basically, switching back and
|
||||
// forth between a tarball fork of version X and the real version X can
|
||||
// confuse NPM. But the main reason to use tarball URLs is to get a fork of
|
||||
// the latest version with some fix, so it's easy to trigger this! So
|
||||
// instead, always use --force. (Even with --force, we still WRITE to the
|
||||
// cache, so we can corrupt the cache for other invocations of npm... ah
|
||||
// well.)
|
||||
var result =
|
||||
this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"),
|
||||
["install", installArg],
|
||||
["install", "--force", installArg],
|
||||
{cwd: dir});
|
||||
|
||||
if (! result.success) {
|
||||
@@ -436,10 +473,11 @@ _.extend(exports, {
|
||||
|
||||
this._ensureConnected();
|
||||
|
||||
// `npm install`, which reads npm-shrinkwrap.json
|
||||
// `npm install`, which reads npm-shrinkwrap.json. See above for why
|
||||
// --force.
|
||||
var result =
|
||||
this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"),
|
||||
["install"], {cwd: dir});
|
||||
["install", "--force"], {cwd: dir});
|
||||
|
||||
|
||||
if (! result.success) {
|
||||
@@ -454,7 +492,7 @@ _.extend(exports, {
|
||||
// dependencies. `npm install` times out after more than a minute.
|
||||
_ensureConnected: function () {
|
||||
try {
|
||||
files.getUrl("http://registry.npmjs.org");
|
||||
httpHelpers.getUrl("http://registry.npmjs.org");
|
||||
} catch (e) {
|
||||
buildmessage.error("Can't install npm dependencies. " +
|
||||
"Are you connected to the internet?");
|
||||
|
||||
@@ -161,6 +161,7 @@ exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callbac
|
||||
var proc = child_process.spawn(mongod_path, [
|
||||
'--bind_ip', '127.0.0.1',
|
||||
'--smallfiles',
|
||||
'--nohttpinterface',
|
||||
'--port', port,
|
||||
'--dbpath', data_path
|
||||
]);
|
||||
|
||||
@@ -26,7 +26,7 @@ var sourcemap = require('source-map');
|
||||
// end up as watched dependencies. (At least for now, packages only used in
|
||||
// target creation (eg minifiers and dev-bundle-fetcher) don't require you to
|
||||
// update BUILT_BY, though you will need to quit and rerun "meteor run".)
|
||||
exports.BUILT_BY = 'meteor/8';
|
||||
exports.BUILT_BY = 'meteor/9';
|
||||
|
||||
// Like Perl's quotemeta: quotes all regexp metacharacters. See
|
||||
// https://github.com/substack/quotemeta/blob/master/index.js
|
||||
|
||||
103
tools/run.js
103
tools/run.js
@@ -108,8 +108,20 @@ var requestQueue = [];
|
||||
var startProxy = function (outerPort, innerPort, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
var httpProxy = require('http-proxy');
|
||||
var p = httpProxy.createServer(function (req, res, proxy) {
|
||||
var http = require('http');
|
||||
// caronte is the code name for http-proxy 1.0 while it's under
|
||||
// development. Once it's released, we may need to adjust the APIs slightly.
|
||||
// (eg, the name of the event on proxy.ev will probably no longer say
|
||||
// "caronte")
|
||||
var caronte = require('caronte');
|
||||
|
||||
var proxy = caronte.createProxyServer({
|
||||
// agent is required to handle keep-alive, and caronte is a little buggy
|
||||
// without it: https://github.com/nodejitsu/node-http-proxy/pull/488
|
||||
agent: new http.Agent({maxSockets: 100})
|
||||
});
|
||||
|
||||
var server = http.createServer(function (req, res) {
|
||||
if (Status.crashing) {
|
||||
// sad face. send error logs.
|
||||
// XXX formatting! text/plain is bad
|
||||
@@ -126,43 +138,35 @@ var startProxy = function (outerPort, innerPort, callback) {
|
||||
});
|
||||
|
||||
res.end();
|
||||
} else if (Status.listening) {
|
||||
// server is listening. things are hunky dory!
|
||||
proxy.proxyRequest(req, res, {
|
||||
host: '127.0.0.1', port: innerPort
|
||||
});
|
||||
} else {
|
||||
// Not listening yet. Queue up request.
|
||||
var buffer = httpProxy.buffer(req);
|
||||
requestQueue.push(function () {
|
||||
proxy.proxyRequest(req, res, {
|
||||
host: '127.0.0.1', port: innerPort,
|
||||
buffer: buffer
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy websocket requests using same buffering logic as for regular HTTP requests
|
||||
p.on('upgrade', function(req, socket, head) {
|
||||
var proxyIt = function () {
|
||||
proxy.web(req, res, {target: 'http://127.0.0.1:' + innerPort});
|
||||
};
|
||||
if (Status.listening) {
|
||||
// server is listening. things are hunky dory!
|
||||
p.proxy.proxyWebSocketRequest(req, socket, head, {
|
||||
host: '127.0.0.1', port: innerPort
|
||||
});
|
||||
proxyIt();
|
||||
} else {
|
||||
// Not listening yet. Queue up request.
|
||||
var buffer = httpProxy.buffer(req);
|
||||
requestQueue.push(function () {
|
||||
p.proxy.proxyWebSocketRequest(req, socket, head, {
|
||||
host: '127.0.0.1', port: innerPort,
|
||||
buffer: buffer
|
||||
});
|
||||
});
|
||||
requestQueue.push(proxyIt);
|
||||
}
|
||||
});
|
||||
|
||||
p.on('error', function (err) {
|
||||
// Proxy websocket requests using same buffering logic as for regular HTTP
|
||||
// requests
|
||||
server.on('upgrade', function(req, socket, head) {
|
||||
var proxyIt = function () {
|
||||
proxy.ws(req, socket, head, { target: 'http://127.0.0.1:' + innerPort});
|
||||
};
|
||||
if (Status.listening) {
|
||||
// server is listening. things are hunky dory!
|
||||
proxyIt();
|
||||
} else {
|
||||
// Not listening yet. Queue up request.
|
||||
requestQueue.push(proxyIt);
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', function (err) {
|
||||
if (err.code == 'EADDRINUSE') {
|
||||
process.stderr.write("Can't listen on port " + outerPort
|
||||
+ ". Perhaps another Meteor is running?\n");
|
||||
@@ -177,17 +181,19 @@ var startProxy = function (outerPort, innerPort, callback) {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// don't spin forever if the app doesn't respond. instead return an
|
||||
// error immediately. This shouldn't happen much since we try to not
|
||||
// send requests if the app is down.
|
||||
p.proxy.on('proxyError', function (err, req, res) {
|
||||
// don't crash if the app doesn't respond. instead return an error
|
||||
// immediately. This shouldn't happen much since we try to not send requests
|
||||
// if the app is down.
|
||||
// XXX should we also handle caronte:outgoing:ws:error, for a failed
|
||||
// websocket?
|
||||
proxy.ee.on('caronte:outgoing:web:error', function (err, req, res) {
|
||||
res.writeHead(503, {
|
||||
'Content-Type': 'text/plain'
|
||||
});
|
||||
res.end('Unexpected error.');
|
||||
});
|
||||
|
||||
p.listen(outerPort, callback);
|
||||
server.listen(outerPort, callback);
|
||||
};
|
||||
|
||||
var saveLog = function (msg) {
|
||||
@@ -402,29 +408,6 @@ exports.run = function (context, options) {
|
||||
("mongodb://127.0.0.1:" + mongoPort + "/meteor");
|
||||
var firstRun = true;
|
||||
|
||||
// node-http-proxy doesn't properly handle errors if it has a problem writing
|
||||
// to the proxy target. While we try to not proxy requests when we don't think
|
||||
// the target is listening, there are race conditions here, and in any case
|
||||
// those attempts don't take effect for pre-existing websocket connections.
|
||||
// Error handling in node-http-proxy is really convoluted and will change with
|
||||
// their ongoing Node 0.10.x compatible rewrite, so rather than trying to
|
||||
// debug and send pull request now, we'll wait for them to finish their
|
||||
// rewrite. In the meantime, ignore two common exceptions that we sometimes
|
||||
// see instead of crashing.
|
||||
//
|
||||
// See https://github.com/meteor/meteor/issues/513
|
||||
//
|
||||
// That bug is about "meteor deploy"s use of http-proxy, but it also affects
|
||||
// our use here; see
|
||||
// https://groups.google.com/d/msg/meteor-core/JgbnfKEa5lA/FJHZtJftfSsJ
|
||||
//
|
||||
// XXX remove this once we've upgraded and fixed http-proxy
|
||||
process.on('uncaughtException', function (e) {
|
||||
if (e && (e.errno === 'EPIPE' || e.message === "This socket is closed."))
|
||||
return;
|
||||
throw e;
|
||||
});
|
||||
|
||||
var serverHandle;
|
||||
var watcher;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ var _ = require('underscore');
|
||||
var sourcemap_support = require('source-map-support');
|
||||
|
||||
// This code is duplicated in tools/server/server.js.
|
||||
var MIN_NODE_VERSION = 'v0.8.24';
|
||||
var MIN_NODE_VERSION = 'v0.10.19';
|
||||
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
|
||||
process.stderr.write(
|
||||
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');
|
||||
|
||||
@@ -172,12 +172,12 @@ assert.doesNotThrow(function () {
|
||||
|
||||
// while bundling, verify that we don't call `npm install
|
||||
// name@version unnecessarily` -- calling `npm install` is enough,
|
||||
// and installing each package separately coul unintentionally bump
|
||||
// and installing each package separately could unintentionally bump
|
||||
// subdependency versions. (to intentionally bump subdependencies,
|
||||
// just remove all of the .npm directory)
|
||||
var bareExecFileSync = meteorNpm._execFileSync;
|
||||
meteorNpm._execFileSync = function(file, args, opts) {
|
||||
if (args[0] === 'install' && args[1])
|
||||
if (args.length > 2 && args[0] === 'install' && args[1] === '--force')
|
||||
assert.fail("shouldn't be installing specific npm packages: " + args[1]);
|
||||
return bareExecFileSync(file, args, opts);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ var testingUpdater = false;
|
||||
var inFiber = require('./fiber-helpers.js').inFiber;
|
||||
var files = require('./files.js');
|
||||
var warehouse = require('./warehouse.js');
|
||||
var httpHelpers = require('./http-helpers.js');
|
||||
|
||||
var manifestUrl = testingUpdater
|
||||
? 'https://s3.amazonaws.com/com.meteor.static/test/update/manifest.json'
|
||||
@@ -21,7 +22,7 @@ exports.getManifest = function (context) {
|
||||
if (context)
|
||||
options.meteorReleaseContext = context;
|
||||
|
||||
return files.getUrl(options);
|
||||
return httpHelpers.getUrl(options);
|
||||
};
|
||||
|
||||
exports.startUpdateChecks = function (context) {
|
||||
|
||||
@@ -28,6 +28,7 @@ var _ = require("underscore");
|
||||
|
||||
var files = require('./files.js');
|
||||
var updater = require('./updater.js');
|
||||
var httpHelpers = require('./http-helpers.js');
|
||||
var fiberHelpers = require('./fiber-helpers.js');
|
||||
var logging = require('./logging.js');
|
||||
|
||||
@@ -235,7 +236,7 @@ _.extend(warehouse, {
|
||||
// after we're done writing packages
|
||||
if (!releaseAlreadyExists) {
|
||||
try {
|
||||
releaseManifestText = files.getUrl(
|
||||
releaseManifestText = httpHelpers.getUrl(
|
||||
WAREHOUSE_URLBASE + "/releases/" + releaseVersion + ".release.json");
|
||||
} catch (e) {
|
||||
// just throw, if we're in the background anyway, or if this is the
|
||||
@@ -303,7 +304,7 @@ _.extend(warehouse, {
|
||||
// try getting the releases's notices. only blessed releases have one, so
|
||||
// if we can't find it just proceed.
|
||||
try {
|
||||
var notices = files.getUrl(
|
||||
var notices = httpHelpers.getUrl(
|
||||
WAREHOUSE_URLBASE + "/releases/" + releaseVersion + ".notices.json");
|
||||
|
||||
// Real notices are valid JSON.
|
||||
@@ -347,7 +348,7 @@ _.extend(warehouse, {
|
||||
"meteor-tools-" + toolsVersion + "-" + platform + ".tar.gz";
|
||||
var toolsTarballPath = "/tools/" + toolsVersion + "/"
|
||||
+ toolsTarballFilename;
|
||||
var toolsTarball = files.getUrl({
|
||||
var toolsTarball = httpHelpers.getUrl({
|
||||
url: WAREHOUSE_URLBASE + toolsTarballPath,
|
||||
encoding: null
|
||||
});
|
||||
@@ -430,7 +431,7 @@ _.extend(warehouse, {
|
||||
"/" + version +
|
||||
"/" + name + '-' + version + "-" + platform + ".tar.gz";
|
||||
|
||||
var tarball = files.getUrl({url: packageUrl, encoding: null});
|
||||
var tarball = httpHelpers.getUrl({url: packageUrl, encoding: null});
|
||||
files.extractTarGz(tarball, packageDir);
|
||||
if (!dontWriteFreshFile)
|
||||
fs.writeFileSync(warehouse.getPackageFreshFile(name, version), '');
|
||||
|
||||
Reference in New Issue
Block a user