Merge branch 'devel' into mongo-upsert

This commit is contained in:
Emily Stark
2013-09-30 15:07:09 -07:00
53 changed files with 1197 additions and 320 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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' })`."
}
]
};

View File

@@ -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).

View File

@@ -335,6 +335,7 @@ var toc = [
"audit-argument-checks",
"backbone",
"bootstrap",
"browser-policy",
"coffeescript",
"d3",
"force-ssl",

View File

@@ -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}}

View 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&lt;ContentType&gt;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&lt;ContentType&gt;DataUrl()"}}
Allows this type of content to be loaded from a `data:` URL.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;SameOrigin()"}}
Allows this type of content to be loaded from the same origin as your app.
{{/dtdd}}
{{#dtdd "BrowserPolicy.disallow&lt;ContentType&gt;()"}}
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>

View File

@@ -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>

View File

@@ -7,4 +7,3 @@ standard-app-packages
autopublish
insecure
preserve-inputs
random

View File

@@ -7,4 +7,3 @@ standard-app-packages
insecure
jquery
preserve-inputs
random

View File

@@ -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(),

View File

@@ -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
View File

@@ -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.

View File

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

View File

@@ -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
View File

@@ -0,0 +1 @@
.build*

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

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

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

View File

@@ -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,

View File

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

View File

@@ -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',

View File

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

View File

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

View File

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

View File

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

View File

@@ -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]);

View File

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

View File

@@ -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

View File

@@ -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",

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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();

View File

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

View File

@@ -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;

View File

@@ -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

View File

@@ -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}}

View File

@@ -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" +

View File

@@ -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' },

View File

@@ -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();

View File

@@ -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
View 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();
}
});

View File

@@ -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');

View File

@@ -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?");

View File

@@ -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
]);

View File

@@ -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

View File

@@ -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;

View File

@@ -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');

View File

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

View File

@@ -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) {

View File

@@ -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), '');