Merge remote-tracking branch 'origin/devel' into blaze-refactor

Conflicts:
	packages/spacebars-tests/template_tests.js
	packages/ui/base.js
	packages/ui/dombackend.js
	packages/ui/dombackend_tests.js
	packages/ui/domrange.js
	packages/ui/domrange_tests.js
	packages/ui/each.js
	packages/ui/render.js
	packages/ui/render_tests.js
This commit is contained in:
David Greenspan
2014-06-23 10:56:55 -07:00
56 changed files with 1312 additions and 840 deletions

View File

@@ -12,6 +12,7 @@ GITHUB: aldeed <eric@dairystatedesigns.com>
GITHUB: AlexeyMK <alexey@alexeymk.com>
GITHUB: apendua <apendua@gmail.com>
GITHUB: arbesfeld <arbesfeld@gmail.com>
GITHUB: Cangit <fredricendrerud@gmail.com>
GITHUB: DenisGorbachev <Denis.Gorbachev@faster-than-wind.ru>
GITHUB: EOT <eot@gmx.at>
GITHUB: FooBarWidget <honglilai@gmail.com>
@@ -20,15 +21,21 @@ GITHUB: OyoKooN <nathan@sxnlabs.com>
GITHUB: RobertLowe <robert@iblargz.com>
GITHUB: ansman <nicklas@ansman.se>
GITHUB: awwx <andrew.wilcox@gmail.com>
GITHUB: babenzele <tim.p.phillips@gmail.com>
GITHUB: cmather <mather.chris@gmail.com>
GITHUB: codeinthehole <david.winterbottom@gmail.com>
GITHUB: dandv <ddascalescu+github@gmail.com>
GITHUB: davegonzalez <gonzalez.dalex@gmail.com>
GITHUB: ducdigital <duc@ducdigital.com>
GITHUB: emgee3 <hello@gravitronic.com>
GITHUB: felixrabe <felix@rabe.io>
GITHUB: FredericoC <frederico.carvalho@3stack.com.au>
GITHUB: icellan <icellan@icellan.com>
GITHUB: jacott <geoffjacobsen@gmail.com>
GITHUB: jfhamlin <jfhamlin@gmail.com>
GITHUB: jbruni <contato@jbruni.com.br>
GITHUB: justinsb <justin@fathomdb.com>
GITHUB: kentonv <temporal@gmail.com>
GITHUB: marcandre <github@marc-andre.ca>
GITHUB: mart-jansink <m.jansink@gmail.com>
GITHUB: meawoppl <meawoppl@gmail.com>
@@ -47,7 +54,10 @@ GITHUB: rgould <rwgould@gmail.com>
GITHUB: ryw <ry@rywalker.com>
GITHUB: rzymek <rzymek@gmail.com>
GITHUB: sdarnell <stephen@darnell.plus.com>
GITHUB: subhog <hubert@orlikarnia.com>
GITHUB: tbjers <torgny@xorcode.com>
GITHUB: timhaines <tmhaines@gmail.com>
GITHUB: tmeasday <tom@thesnail.org>
GITHUB: yeputons <egor.suvorov@gmail.com>
GITHUB: zol <zol@percolatestudio.com>
@@ -65,4 +75,3 @@ METEOR: sixolet <naomi@meteor.com>
METEOR: Slava <slava@meteor.com>
METEOR: stubailo <sashko@mit.edu>
METEOR: ekatek <ekate@meteor.com>

View File

@@ -1,19 +1,140 @@
## v.NEXT
* Upgraded dependencies:
- less: 1.7.1 (from 1.6.1)
## v0.8.2
#### Meteor Accounts
* Switch `accounts-password` to use bcrypt to store passwords on the
server. (Previous versions of Meteor used a protocol called SRP.)
Users will be transparently transitioned when they log in. This
transition is one-way, so you cannot downgrade a production app once
you upgrade to 0.8.2. If you are maintaining an authenticating DDP
client:
- Clients that use the plaintext password login handler (i.e. call
the `login` method with argument `{ password: <plaintext
password> }`) will continue to work, but users will not be
transitioned from SRP to bcrypt when logging in with this login
handler.
- Clients that use SRP will no longer work. These clients should
instead directly call the `login` method, as in
`Meteor.loginWithPassword`. The argument to the `login` method
can be either:
- `{ password: <plaintext password> }`, or
- `{ password: { digest: <password hash>, algorithm: "sha-256" } }`,
where the password hash is the hex-encoded SHA256 hash of the
plaintext password.
* Show the display name of the currently logged-in user after following
an email verification link or a password reset link in `accounts-ui`.
* Add a `userEmail` option to `Meteor.loginWithMeteorDeveloperAccount`
to pre-fill the user's email address in the OAuth popup.
* Ensure that the user object has updated token information before
it is passed to email template functions. #2210
* Export the function that serves the HTTP response at the end of an
OAuth flow as `OAuth._endOfLoginResponse`. This function can be
overridden to make the OAuth popup flow work in certain mobile
environments where `window.opener` is not supported.
* Remove support for OAuth redirect URLs with a `redirect` query
parameter. This OAuth flow was never documented and never fully
worked.
#### Blaze
* Blaze now tracks individual CSS rules in `style` attributes and won't
overwrite changes to them made by other JavaScript libraries.
* Add {{> UI.dynamic}} to make it easier to dynamically render a
template with a data context.
* Add `UI._templateInstance()` for accessing the current template
instance from within a block helper.
* Add `UI._parentData(n)` for accessing parent data contexts from
within a block helper.
* Add preliminary API for registering hooks to run when Blaze intends to
insert, move, or remove DOM elements. For example, you can use these
hooks to animate nodes as they are inserted, moved, or removed. To use
them, you can set the `_uihooks` property on a container DOM
element. `_uihooks` is an object that can have any subset of the
following three properties:
- `insertElement: function (node, next)`: called when Blaze intends
to insert the DOM element `node` before the element `next`
- `moveElement: function (node, next)`: called when Blaze intends to
move the DOM element `node` before the element `next`
- `removeElement: function (node)`: called when Blaze intends to
remove the DOM element `node`
Note that when you set one of these functions on a container
element, Blaze will not do the actual operation; it's your
responsibility to actually insert, move, or remove the node (by
calling `$(node).remove()`, for example).
* The `findAll` method on template instances now returns a vanilla
array, not a jQuery object. The `$` method continues to
return a jQuery object. #2039
* Fix a Blaze memory leak by cleaning up event handlers when a template
instance is destroyed. #1997
* Fix a bug where helpers used by {{#with}} were still re-running when
their reactive data sources changed after they had been removed from
the DOM.
* Stop not updating form controls if they're focused. If a field is
edited by one user while another user is focused on it, it will just
lose its value but maintain its focus. #1965
* Add `_nestInCurrentComputation` option to `UI.render`, fixing a bug in
{{#each}} when an item is added inside a computation that subsequently
gets invalidated. #2156
* Fix bug where "=" was not allowed in helper arguments. #2157
* Fix bug when a template tag immediately follows a Spacebars block
comment. #2175
#### Command-line tool
* Add --directory flag to `meteor bundle`. Setting this flag outputs a
directory rather than a tarball.
* Speed up updates of NPM modules by upgrading Node to include our fix for
https://github.com/npm/npm/issues/3265 instead of passing `--force` to
`npm install`.
* Always rebuild on changes to npm-shrinkwrap.json files. #1648
* Fix uninformative error message when deploying to long hostnames. #1208
* Increase a buffer size to avoid failing when running MongoDB due to a
large number of processes running on the machine, and fix the error
message when the failure does occur. #2158
* Clarify a `meteor mongo` error message when using the MONGO_URL
environment variable. #1256
#### Testing
* Run server tests from multiple clients serially instead of in
parallel. This allows testing features that modify global server
state. #2088
#### Security
* Add Content-Type headers on JavaScript and CSS resources.
* Add `X-Content-Type-Options: nosniff` header to
@@ -21,18 +142,11 @@
`browser-policy-content` and you don't want your app to send this
header, then call `BrowserPolicy.content.allowContentTypeSniffing()`.
* Fix memory leak (introduced in 0.8.1) by making sure to unregister
sessions at the server when they are closed due to heartbeat timeout.
* Use `Meteor.absoluteUrl()` to compute the redirect URL in the `force-ssl`
package (instead of the host header).
* Fix hardcoded Twitter URL in `oauth1` package. This fixes a regression
in 0.8.0.1 that broke Atmosphere packages that do OAuth1
logins. #2154.
* Add `credentialSecret` argument to `Google.retrieveCredential`, which
was forgotten in a previous release.
* Fix a Blaze memory leak by cleaning up event handlers when a template
instance is destroyed. #1997
#### Miscellaneous
* Allow `check` to work on the server outside of a Fiber. #2136
@@ -41,26 +155,39 @@
* The legacy polling observe driver handles errors communicating with MongoDB
better and no longer gets "stuck" in some circumstances.
* Add {{> UI.dynamic}} to make it easier to dynamically render a
template with a data context. XXX Update "Using Blaze" wiki page.
* Show the display name of the currently logged-in user after following
a verification link or password reset link in `accounts-ui`.
* Use `Meteor.absoluteUrl()` to compute the redirect URI in `force-ssl`
instead of the host header.
* Automatically rewind cursors before calls to `fetch`, `forEach`, or `map`. On
the client, don't cache the return value of `cursor.count()` (consistently
with the server behavior). `cursor.rewind()` is now a no-op. #2114
* Remove an obsolete hack in reporting line numbers for LESS errors. #2216
* Avoid exceptions when accessing localStorage in certain Internet
Explorer configurations. #1291, #1688.
* Make `handle.ready()` reactively stop, where `handle` is a
subscription handle.
* Fix an error message from `audit-argument-checks` after login.
* Make the DDP server send an error if the client sends a connect
message with a missing or malformed `support` field. #2125
* Fix missing `jquery` dependency in the `amplify` package. #2113
* Ban inserting EJSON custom types as documents. #2095
* Fix incorrect URL rewrites in stylesheets. #2106
* Upgraded dependencies:
- node: 0.10.28 (from 0.10.26)
- uglify-js: 2.4.13 (from 2.4.7)
- sockjs server: 0.3.9 (from 0.3.8)
- websocket-driver: 0.3.4 (from 0.3.2)
- stylus: 0.46.3 (from 0.42.3)
Patches contributed by GitHub users awwx, subhog
Patches contributed by GitHub users awwx, babenzele, Cangit, dandv,
ducdigital, emgee3, felixrabe, FredericoC, jbruni, kentonv, mizzao,
mquandalle, subhog, tbjers, tmeasday.
## v.0.8.1.3

View File

@@ -1 +1 @@
0.8.1.3
0.8.2

View File

@@ -3163,8 +3163,3 @@ code can read `data.txt` by running:
{{/each}}
</dl>
</template>
<template name="api_section">
<h2 id="{{id}}"><a href="#{{id}}" class="selflink"><span>{{name}}</span></a></h2>
</template>

View File

@@ -1125,6 +1125,11 @@ Template.api.loginWithExternalService = {
name: "forceApprovalPrompt",
type: "Boolean",
descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google."
},
{
name: "userEmail",
type: "String",
descr: "An email address that the external service will use to pre-fill the login prompt. Currently only supported with Meteor developer accounts."
}
]
};

View File

@@ -2,7 +2,7 @@
<div>
{{#markdown}}
{{#api_section "commandline"}}Command line{{/api_section}}
<h1 id="commandline">Command line</h1>
<!-- XXX some intro text? -->

View File

@@ -740,7 +740,7 @@ To get started, run
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.10 and a
MongoDB server. (The current release of Meteor has been tested with Node
0.10.28; older versions contain a serious bug that can cause production servers
0.10.29; older versions contain a serious bug that can cause production servers
to stall.) 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

View File

@@ -1,5 +1,5 @@
// While galaxy apps are on their own special meteor releases, override
// Meteor.release here.
if (Meteor.isClient) {
Meteor.release = Meteor.release ? "0.8.1.3" : undefined;
Meteor.release = Meteor.release ? "0.8.2" : undefined;
}

View File

@@ -1 +1 @@
0.8.1.3
0.8.2

View File

@@ -1 +1 @@
0.8.1.3
0.8.2

View File

@@ -1 +1 @@
0.8.1.3
0.8.2

View File

@@ -1 +1 @@
0.8.1.3
0.8.2

View File

@@ -1 +1 @@
0.8.1.3
0.8.2

2
meteor
View File

@@ -1,6 +1,6 @@
#!/bin/bash
BUNDLE_VERSION=0.3.37
BUNDLE_VERSION=0.3.38
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.

View File

@@ -544,7 +544,7 @@ Meteor.methods({
/// ACCOUNT DATA
///
// connectionId -> {connection, loginToken, srpChallenge}
// connectionId -> {connection, loginToken}
var accountData = {};
// HACK: This is used by 'meteor-accounts' to get the loginToken for a
@@ -1023,7 +1023,7 @@ Accounts.insertUserDoc = function (options, user) {
// XXX string parsing sucks, maybe
// https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day
if (e.name !== 'MongoError') throw e;
var match = e.err.match(/^E11000 duplicate key error index: ([^ ]+)/);
var match = e.err.match(/E11000 duplicate key error index: ([^ ]+)/);
if (!match) throw e;
if (match[1].indexOf('$emails.address') !== -1)
throw new Meteor.Error(403, "Email already exists.");

View File

@@ -10,7 +10,7 @@ if (Meteor.isClient) {
var credentialRequestCompleteCallback =
Accounts.oauth.credentialRequestCompleteHandler(callback);
MeteorDeveloperAccounts.requestCredential(credentialRequestCompleteCallback);
MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({

View File

@@ -7,6 +7,7 @@ Package.on_use(function(api) {
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
api.use('srp', ['client', 'server']);
api.use('sha', ['client', 'server']);
api.use('email', ['server']);
api.use('random', ['server']);
api.use('check', ['server']);

View File

@@ -8,41 +8,80 @@
// @param password {String}
// @param callback {Function(error|undefined)}
Meteor.loginWithPassword = function (selector, password, callback) {
var srp = new SRP.Client(password);
var request = srp.startExchange();
if (typeof selector === 'string')
if (selector.indexOf('@') === -1)
selector = {username: selector};
else
selector = {email: selector};
request.user = selector;
Accounts.callLoginMethod({
methodArguments: [{
user: selector,
password: hashPassword(password)
}],
userCallback: function (error, result) {
if (error && error.error === 400 &&
error.reason === 'old password format') {
// The "reason" string should match the error thrown in the
// password login handler in password_server.js.
// Normally, we only set Meteor.loggingIn() to true within
// Accounts.callLoginMethod, but we'd also like it to be true during the
// password exchange. So we set it to true here, and clear it on error; in
// the non-error case, it gets cleared by callLoginMethod.
Accounts._setLoggingIn(true);
Accounts.connection.apply(
'beginPasswordExchange', [request], function (error, result) {
if (error || !result) {
Accounts._setLoggingIn(false);
error = error ||
new Error("No result from call to beginPasswordExchange");
callback && callback(error);
return;
// XXX COMPAT WITH 0.8.1.3
// If this user's last login was with a previous version of
// Meteor that used SRP, then the server throws this error to
// indicate that we should try again. The error includes the
// user's SRP identity. We provide a value derived from the
// identity and the password to prove to the server that we know
// the password without requiring a full SRP flow, as well as
// SHA256(password), which the server bcrypts and stores in
// place of the old SRP information for this user.
srpUpgradePath({
upgradeError: error,
userSelector: selector,
plaintextPassword: password
}, callback);
}
else if (error) {
callback(error);
} else {
callback();
}
}
});
};
var response = srp.respondToChallenge(result);
Accounts.callLoginMethod({
methodArguments: [{srp: response}],
validateResult: function (result) {
if (!srp.verifyConfirmation({HAMK: result.HAMK}))
throw new Error("Server is cheating!");
},
userCallback: callback});
var hashPassword = function (password) {
return {
digest: SHA256(password),
algorithm: "sha-256"
};
};
// XXX COMPAT WITH 0.8.1.3
// The server requested an upgrade from the old SRP password format,
// so supply the needed SRP identity to login. Options:
// - upgradeError: the error object that the server returned to tell
// us to upgrade from SRP to bcrypt.
// - userSelector: selector to retrieve the user object
// - plaintextPassword: the password as a string
var srpUpgradePath = function (options, callback) {
var details;
try {
details = EJSON.parse(options.upgradeError.details);
} catch (e) {}
if (!(details && details.format === 'srp')) {
callback(new Meteor.Error(400,
"Password is old. Please reset your " +
"password."));
} else {
Accounts.callLoginMethod({
methodArguments: [{
user: options.userSelector,
srp: SHA256(details.identity + ":" + options.plaintextPassword),
password: hashPassword(options.plaintextPassword)
}],
userCallback: callback
});
}
};
@@ -52,10 +91,9 @@ Accounts.createUser = function (options, callback) {
if (!options.password)
throw new Error("Must set options.password");
var verifier = SRP.generateVerifier(options.password);
// strip old password, replacing with the verifier object
delete options.password;
options.srp = verifier;
// Replace password with the hashed password.
options.password = hashPassword(options.password);
Accounts.callLoginMethod({
methodName: 'createUser',
@@ -79,49 +117,39 @@ Accounts.changePassword = function (oldPassword, newPassword, callback) {
return;
}
var verifier = SRP.generateVerifier(newPassword);
if (!oldPassword) {
Accounts.connection.apply(
'changePassword', [{srp: verifier}], function (error, result) {
if (error || !result) {
callback && callback(
error || new Error("No result from changePassword."));
} else {
callback && callback();
}
});
} else { // oldPassword
var srp = new SRP.Client(oldPassword);
var request = srp.startExchange();
request.user = {id: Meteor.user()._id};
Accounts.connection.apply(
'beginPasswordExchange', [request], function (error, result) {
if (error || !result) {
callback && callback(
error || new Error("No result from call to beginPasswordExchange"));
return;
}
var response = srp.respondToChallenge(result);
response.srp = verifier;
Accounts.connection.apply(
'changePassword', [response],function (error, result) {
if (error || !result) {
callback && callback(
error || new Error("No result from changePassword."));
Accounts.connection.apply(
'changePassword',
[oldPassword ? hashPassword(oldPassword) : null, hashPassword(newPassword)],
function (error, result) {
if (error || !result) {
if (error && error.error === 400 &&
error.reason === 'old password format') {
// XXX COMPAT WITH 0.8.1.3
// The server is telling us to upgrade from SRP to bcrypt, as
// in Meteor.loginWithPassword.
srpUpgradePath({
upgradeError: error,
userSelector: { id: Meteor.userId() },
plaintextPassword: oldPassword
}, function (err) {
if (err) {
callback(err);
} else {
if (!srp.verifyConfirmation(result)) {
// Monkey business!
callback &&
callback(new Error("Old password verification failed."));
} else {
callback && callback();
}
// Now that we've successfully migrated from srp to
// bcrypt, try changing the password again.
Accounts.changePassword(oldPassword, newPassword, callback);
}
});
});
}
} else {
// A normal error, not an error telling us to upgrade to bcrypt
callback && callback(
error || new Error("No result from changePassword."));
}
} else {
callback && callback();
}
}
);
};
// Sends an email to a user with a link that can be used to reset
@@ -148,10 +176,9 @@ Accounts.resetPassword = function(token, newPassword, callback) {
if (!newPassword)
throw new Error("Need to pass newPassword");
var verifier = SRP.generateVerifier(newPassword);
Accounts.callLoginMethod({
methodName: 'resetPassword',
methodArguments: [token, verifier],
methodArguments: [token, hashPassword(newPassword)],
userCallback: callback});
};

View File

@@ -1,3 +1,78 @@
/// BCRYPT
var bcrypt = Npm.require('bcrypt');
var bcryptHash = Meteor._wrapAsync(bcrypt.hash);
var bcryptCompare = Meteor._wrapAsync(bcrypt.compare);
// User records have a 'services.password.bcrypt' field on them to hold
// their hashed passwords (unless they have a 'services.password.srp'
// field, in which case they will be upgraded to bcrypt the next time
// they log in).
//
// When the client sends a password to the server, it can either be a
// string (the plaintext password) or an object with keys 'digest' and
// 'algorithm' (must be "sha-256" for now). The Meteor client always sends
// password objects { digest: *, algorithm: "sha-256" }, but DDP clients
// that don't have access to SHA can just send plaintext passwords as
// strings.
//
// When the server receives a plaintext password as a string, it always
// hashes it with SHA256 before passing it into bcrypt. When the server
// receives a password as an object, it asserts that the algorithm is
// "sha-256" and then passes the digest to bcrypt.
Accounts._bcryptRounds = 10;
// Given a 'password' from the client, extract the string that we should
// bcrypt. 'password' can be one of:
// - String (the plaintext password)
// - Object with 'digest' and 'algorithm' keys. 'algorithm' must be "sha-256".
//
var getPasswordString = function (password) {
if (typeof password === "string") {
password = SHA256(password);
} else { // 'password' is an object
if (password.algorithm !== "sha-256") {
throw new Error("Invalid password hash algorithm. " +
"Only 'sha-256' is allowed.");
}
password = password.digest;
}
return password;
};
// Use bcrypt to hash the password for storage in the database.
// `password` can be a string (in which case it will be run through
// SHA256 before bcrypt) or an object with properties `digest` and
// `algorithm` (in which case we bcrypt `password.digest`).
//
var hashPassword = function (password) {
password = getPasswordString(password);
return bcryptHash(password, Accounts._bcryptRounds);
};
// Check whether the provided password matches the bcrypt'ed password in
// the database user record. `password` can be a string (in which case
// it will be run through SHA256 before bcrypt) or an object with
// properties `digest` and `algorithm` (in which case we bcrypt
// `password.digest`).
//
Accounts._checkPassword = function (user, password) {
var result = {
userId: user._id
};
password = getPasswordString(password);
if (! bcryptCompare(password, user.services.password.bcrypt)) {
result.error = new Meteor.Error(403, "Incorrect password");
}
return result;
};
var checkPassword = Accounts._checkPassword;
///
/// LOGIN
///
@@ -16,6 +91,16 @@ var selectorFromUserQuery = function (user) {
throw new Error("shouldn't happen (validation missed something)");
};
var findUserFromUserQuery = function (user) {
var selector = selectorFromUserQuery(user);
var user = Meteor.users.findOne(selector);
if (!user)
throw new Meteor.Error(403, "User not found");
return user;
};
// XXX maybe this belongs in the check package
var NonEmptyString = Match.Where(function (x) {
check(x, String);
@@ -33,134 +118,133 @@ var userQueryValidator = Match.Where(function (user) {
return true;
});
// Step 1 of SRP password exchange. This puts an `M` value in the
// session data for this connection. If a client later sends the same
// `M` value to a method on this connection, it proves they know the
// password for this user. We can then prove we know the password to
// them by sending our `HAMK` value.
var passwordValidator = Match.OneOf(
String,
{ digest: String, algorithm: String }
);
// Handler to login with a password.
//
// The Meteor client sets options.password to an object with keys
// 'digest' (set to SHA256(password)) and 'algorithm' ("sha-256").
//
// For other DDP clients which don't have access to SHA, the handler
// also accepts the plaintext password in options.password as a string.
//
// (It might be nice if servers could turn the plaintext password
// option off. Or maybe it should be opt-in, not opt-out?
// Accounts.config option?)
//
// Note that neither password option is secure without SSL.
//
// @param request {Object} with fields:
// user: either {username: (username)}, {email: (email)}, or {id: (userId)}
// A: hex encoded int. the client's public key for this exchange
// @returns {Object} with fields:
// identity: random string ID
// salt: random string ID
// B: hex encoded int. server's public key for this exchange
Meteor.methods({beginPasswordExchange: function (request) {
var self = this;
try {
check(request, {
user: userQueryValidator,
A: String
});
var selector = selectorFromUserQuery(request.user);
var user = Meteor.users.findOne(selector);
if (!user)
throw new Meteor.Error(403, "User not found");
if (!user.services || !user.services.password ||
!user.services.password.srp)
throw new Meteor.Error(403, "User has no password set");
var verifier = user.services.password.srp;
var srp = new SRP.Server(verifier);
var challenge = srp.issueChallenge({A: request.A});
} catch (err) {
// Report login failure if the method fails, so that login hooks are
// called. If the method succeeds, login hooks will be called when
// the second step method ('login') is called. If a user calls
// 'beginPasswordExchange' but then never calls the second step
// 'login' method, no login hook will fire.
// The validate login hooks can mutate the exception to be thrown.
var attempt = Accounts._reportLoginFailure(self, 'beginPasswordExchange', arguments, {
type: 'password',
error: err,
userId: user && user._id
});
throw attempt.error;
}
// Save results so we can verify them later.
Accounts._setAccountData(this.connection.id, 'srpChallenge',
{ userId: user._id, M: srp.M, HAMK: srp.HAMK }
);
return challenge;
}});
// Handler to login with password via SRP. Checks the `M` value set by
// beginPasswordExchange.
Accounts.registerLoginHandler("password", function (options) {
if (!options.srp)
return undefined; // don't handle
check(options.srp, {M: String});
// we're always called from within a 'login' method, so this should
// be safe.
var currentInvocation = DDP._CurrentInvocation.get();
var serialized = Accounts._getAccountData(currentInvocation.connection.id, 'srpChallenge');
if (!serialized || serialized.M !== options.srp.M)
return {
userId: serialized && serialized.userId,
error: new Meteor.Error(403, "Incorrect password")
};
// Only can use challenges once.
Accounts._setAccountData(currentInvocation.connection.id, 'srpChallenge', undefined);
var userId = serialized.userId;
var user = Meteor.users.findOne(userId);
// Was the user deleted since the start of this challenge?
if (!user)
return {
userId: userId,
error: new Meteor.Error(403, "User not found")
};
return {
userId: userId,
options: {HAMK: serialized.HAMK}
};
});
// Handler to login with plaintext password.
//
// The meteor client doesn't use this, it is for other DDP clients who
// haven't implemented SRP. Since it sends the password in plaintext
// over the wire, it should only be run over SSL!
//
// Also, it might be nice if servers could turn this off. Or maybe it
// should be opt-in, not opt-out? Accounts.config option?
Accounts.registerLoginHandler("password", function (options) {
if (!options.password || !options.user)
if (! options.password || options.srp)
return undefined; // don't handle
check(options, {user: userQueryValidator, password: String});
check(options, {
user: userQueryValidator,
password: passwordValidator
});
var selector = selectorFromUserQuery(options.user);
var user = Meteor.users.findOne(selector);
if (!user)
throw new Meteor.Error(403, "User not found");
var user = findUserFromUserQuery(options.user);
if (!user.services || !user.services.password ||
!user.services.password.srp)
return {
userId: user._id,
error: new Meteor.Error(403, "User has no password set")
};
!(user.services.password.bcrypt || user.services.password.srp))
throw new Meteor.Error(403, "User has no password set");
// Just check the verifier output when the same identity and salt
// are passed. Don't bother with a full exchange.
var verifier = user.services.password.srp;
var newVerifier = SRP.generateVerifier(options.password, {
identity: verifier.identity, salt: verifier.salt});
if (!user.services.password.bcrypt) {
if (typeof options.password === "string") {
// The client has presented a plaintext password, and the user is
// not upgraded to bcrypt yet. We don't attempt to tell the client
// to upgrade to bcrypt, because it might be a standalone DDP
// client doesn't know how to do such a thing.
var verifier = user.services.password.srp;
var newVerifier = SRP.generateVerifier(options.password, {
identity: verifier.identity, salt: verifier.salt});
if (verifier.verifier !== newVerifier.verifier)
if (verifier.verifier !== newVerifier.verifier) {
return {
userId: user._id,
error: new Meteor.Error(403, "Incorrect password")
};
}
return {userId: user._id};
} else {
// Tell the client to use the SRP upgrade process.
throw new Meteor.Error(400, "old password format", EJSON.stringify({
format: 'srp',
identity: user.services.password.srp.identity
}));
}
}
return checkPassword(
user,
options.password
);
});
// Handler to login using the SRP upgrade path. To use this login
// handler, the client must provide:
// - srp: H(identity + ":" + password)
// - password: a string or an object with properties 'digest' and 'algorithm'
//
// We use `options.srp` to verify that the client knows the correct
// password without doing a full SRP flow. Once we've checked that, we
// upgrade the user to bcrypt and remove the SRP information from the
// user document.
//
// The client ends up using this login handler after trying the normal
// login handler (above), which throws an error telling the client to
// try the SRP upgrade path.
//
// XXX COMPAT WITH 0.8.1.3
Accounts.registerLoginHandler("password", function (options) {
if (!options.srp || !options.password)
return undefined; // don't handle
check(options, {
user: userQueryValidator,
srp: String,
password: passwordValidator
});
var user = findUserFromUserQuery(options.user);
// Check to see if another simultaneous login has already upgraded
// the user record to bcrypt.
if (user.services && user.services.password && user.services.password.bcrypt)
return checkPassword(user, options.password);
if (!(user.services && user.services.password && user.services.password.srp))
throw new Meteor.Error(403, "User has no password set");
var v1 = user.services.password.srp.verifier;
var v2 = SRP.generateVerifier(
null,
{
hashedIdentityAndPassword: options.srp,
salt: user.services.password.srp.salt
}
).verifier;
if (v1 !== v2)
return {
userId: user._id,
error: new Meteor.Error(403, "Incorrect password")
};
// Upgrade to bcrypt on successful login.
var salted = hashPassword(options.password);
Meteor.users.update(
user._id,
{
$unset: { 'services.password.srp': 1 },
$set: { 'services.password.bcrypt': salted }
}
);
return {userId: user._id};
});
@@ -170,34 +254,47 @@ Accounts.registerLoginHandler("password", function (options) {
///
// Let the user change their own password if they know the old
// password. Checks the `M` value set by beginPasswordExchange.
Meteor.methods({changePassword: function (options) {
// password. `oldPassword` and `newPassword` should be objects with keys
// `digest` and `algorithm` (representing the SHA256 of the password).
//
// XXX COMPAT WITH 0.8.1.3
// Like the login method, if the user hasn't been upgraded from SRP to
// bcrypt yet, then this method will throw an 'old password format'
// error. The client should call the SRP upgrade login handler and then
// retry this method again.
//
// UNLIKE the login method, there is no way to avoid getting SRP upgrade
// errors thrown. The reasoning for this is that clients using this
// method directly will need to be updated anyway because we no longer
// support the SRP flow that they would have been doing to use this
// method previously.
Meteor.methods({changePassword: function (oldPassword, newPassword) {
check(oldPassword, passwordValidator);
check(newPassword, passwordValidator);
if (!this.userId)
throw new Meteor.Error(401, "Must be logged in");
check(options, {
// If options.M is set, it means we went through a challenge with the old
// password. For now, we don't allow changePassword without knowing the old
// password.
M: String,
srp: Match.Optional(SRP.matchVerifier),
password: Match.Optional(String)
});
var serialized = Accounts._getAccountData(this.connection.id, 'srpChallenge');
if (!serialized || serialized.M !== options.M)
throw new Meteor.Error(403, "Incorrect password");
if (serialized.userId !== this.userId)
// No monkey business!
throw new Meteor.Error(403, "Incorrect password");
// Only can use challenges once.
Accounts._setAccountData(this.connection.id, 'srpChallenge', undefined);
var user = Meteor.users.findOne(this.userId);
if (!user)
throw new Meteor.Error(403, "User not found");
var verifier = options.srp;
if (!verifier && options.password) {
verifier = SRP.generateVerifier(options.password);
if (!user.services || !user.services.password ||
(!user.services.password.bcrypt && !user.services.password.srp))
throw new Meteor.Error(403, "User has no password set");
if (! user.services.password.bcrypt) {
throw new Meteor.Error(400, "old password format", EJSON.stringify({
format: 'srp',
identity: user.services.password.srp.identity
}));
}
if (!verifier)
throw new Meteor.Error(400, "Invalid verifier");
var result = checkPassword(user, oldPassword);
if (result.error)
throw result.error;
var hashed = hashPassword(newPassword);
// It would be better if this removed ALL existing tokens and replaced
// the token for the current connection with a new one, but that would
@@ -207,29 +304,28 @@ Meteor.methods({changePassword: function (options) {
Meteor.users.update(
{ _id: this.userId },
{
$set: { 'services.password.srp': verifier },
$set: { 'services.password.bcrypt': hashed },
$pull: {
'services.resume.loginTokens': { hashedToken: { $ne: currentToken } }
}
}
);
var ret = {passwordChanged: true};
if (serialized)
ret.HAMK = serialized.HAMK;
return ret;
return {passwordChanged: true};
}});
// Force change the users password.
Accounts.setPassword = function (userId, newPassword) {
Accounts.setPassword = function (userId, newPlaintextPassword) {
var user = Meteor.users.findOne(userId);
if (!user)
throw new Meteor.Error(403, "User not found");
var newVerifier = SRP.generateVerifier(newPassword);
Meteor.users.update({_id: user._id}, {
$set: {'services.password.srp': newVerifier}});
Meteor.users.update(
{_id: user._id},
{ $unset: {'services.password.srp': 1}, // XXX COMPAT WITH 0.8.1.3
$set: {'services.password.bcrypt': hashPassword(newPlaintextPassword)} }
);
};
@@ -266,13 +362,16 @@ Accounts.sendResetPasswordEmail = function (userId, email) {
var token = Random.secret();
var when = new Date();
var tokenRecord = {
token: token,
email: email,
when: when
};
Meteor.users.update(userId, {$set: {
"services.password.reset": {
token: token,
email: email,
when: when
}
"services.password.reset": tokenRecord
}});
// before passing to template, update user object with new token
Meteor._ensure(user, 'services', 'password').reset = tokenRecord;
var resetPasswordUrl = Accounts.urls.resetPassword(token);
@@ -312,17 +411,20 @@ Accounts.sendEnrollmentEmail = function (userId, email) {
if (!email || !_.contains(_.pluck(user.emails || [], 'address'), email))
throw new Error("No such email for user.");
var token = Random.secret();
var when = new Date();
var tokenRecord = {
token: token,
email: email,
when: when
};
Meteor.users.update(userId, {$set: {
"services.password.reset": {
token: token,
email: email,
when: when
}
"services.password.reset": tokenRecord
}});
// before passing to template, update user object with new token
Meteor._ensure(user, 'services', 'password').reset = tokenRecord;
var enrollAccountUrl = Accounts.urls.enrollAccount(token);
var options = {
@@ -342,7 +444,7 @@ Accounts.sendEnrollmentEmail = function (userId, email) {
// Take token from sendResetPasswordEmail or sendEnrollmentEmail, change
// the users password, and log them in.
Meteor.methods({resetPassword: function (token, newVerifier) {
Meteor.methods({resetPassword: function (token, newPassword) {
var self = this;
return Accounts._loginMethod(
self,
@@ -351,10 +453,10 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
"password",
function () {
check(token, String);
check(newVerifier, SRP.matchVerifier);
check(newPassword, passwordValidator);
var user = Meteor.users.findOne({
"services.password.reset.token": ""+token});
"services.password.reset.token": token});
if (!user)
throw new Meteor.Error(403, "Token expired");
var email = user.services.password.reset.email;
@@ -364,6 +466,8 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
error: new Meteor.Error(403, "Token has invalid email address")
};
var hashed = hashPassword(newPassword);
// NOTE: We're about to invalidate tokens on the user, who we might be
// logged in as. Make sure to avoid logging ourselves out if this
// happens. But also make sure not to leave the connection in a state
@@ -376,7 +480,7 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
try {
// Update the user record by:
// - Changing the password verifier to the new one
// - Changing the password to the new one
// - Forgetting about the reset token that was just used
// - Verifying their email, since they got the password reset via email.
var affectedRecords = Meteor.users.update(
@@ -385,9 +489,10 @@ Meteor.methods({resetPassword: function (token, newVerifier) {
'emails.address': email,
'services.password.reset.token': token
},
{$set: {'services.password.srp': newVerifier,
{$set: {'services.password.bcrypt': hashed,
'emails.$.verified': true},
$unset: {'services.password.reset': 1}});
$unset: {'services.password.reset': 1,
'services.password.srp': 1}});
if (affectedRecords !== 1)
return {
userId: user._id,
@@ -443,6 +548,13 @@ Accounts.sendVerificationEmail = function (userId, address) {
{_id: userId},
{$push: {'services.email.verificationTokens': tokenRecord}});
// before passing to template, update user object with new token
Meteor._ensure(user, 'services', 'email');
if (!user.services.email.verificationTokens) {
user.services.email.verificationTokens = [];
}
user.services.email.verificationTokens.push(tokenRecord);
var verifyEmailUrl = Accounts.urls.verifyEmail(tokenRecord.token);
var options = {
@@ -528,8 +640,7 @@ var createUser = function (options) {
check(options, Match.ObjectIncluding({
username: Match.Optional(String),
email: Match.Optional(String),
password: Match.Optional(String),
srp: Match.Optional(SRP.matchVerifier)
password: Match.Optional(passwordValidator)
}));
var username = options.username;
@@ -537,18 +648,12 @@ var createUser = function (options) {
if (!username && !email)
throw new Meteor.Error(400, "Need to set a username or email");
// Raw password. The meteor client doesn't send this, but a DDP
// client that didn't implement SRP could send this. This should
// only be done over SSL.
var user = {services: {}};
if (options.password) {
if (options.srp)
throw new Meteor.Error(400, "Don't pass both password and srp in options");
options.srp = SRP.generateVerifier(options.password);
var hashed = hashPassword(options.password);
user.services.password = { bcrypt: hashed };
}
var user = {services: {}};
if (options.srp)
user.services.password = {srp: options.srp}; // XXX validate verifier
if (username)
user.username = username;
if (email)

View File

@@ -732,6 +732,86 @@ if (Meteor.isClient) (function () {
}
]);
testAsyncMulti("passwords - srp to bcrypt upgrade", [
logoutStep,
// Create user with old SRP credentials in the database.
function (test, expect) {
var self = this;
Meteor.call("testCreateSRPUser", expect(function (error, result) {
test.isFalse(error);
self.username = result;
}));
},
// We are able to login with the old style credentials in the database.
function (test, expect) {
Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) {
test.isFalse(error);
}));
},
function (test, expect) {
Meteor.call("testSRPUpgrade", this.username, expect(function (error) {
test.isFalse(error);
}));
},
logoutStep,
// After the upgrade to bcrypt we're still able to login.
function (test, expect) {
Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) {
test.isFalse(error);
}));
},
logoutStep,
function (test, expect) {
Meteor.call("removeUser", this.username, expect(function (error) {
test.isFalse(error);
}));
}
]);
testAsyncMulti("passwords - srp to bcrypt upgrade via password change", [
logoutStep,
// Create user with old SRP credentials in the database.
function (test, expect) {
var self = this;
Meteor.call("testCreateSRPUser", expect(function (error, result) {
test.isFalse(error);
self.username = result;
}));
},
// Log in with the plaintext password handler, which should NOT upgrade us to bcrypt.
function (test, expect) {
Accounts.callLoginMethod({
methodName: "login",
methodArguments: [ { user: { username: this.username }, password: "abcdef" } ],
userCallback: expect(function (err) {
test.isFalse(err);
})
});
},
function (test, expect) {
Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) {
test.isFalse(error);
}));
},
// Changing our password should upgrade us to bcrypt.
function (test, expect) {
Accounts.changePassword("abcdef", "abcdefg", expect(function (error) {
test.isFalse(error);
}));
},
function (test, expect) {
Meteor.call("testSRPUpgrade", this.username, expect(function (error) {
test.isFalse(error);
}));
},
// And after the upgrade we should be able to change our password again.
function (test, expect) {
Accounts.changePassword("abcdefg", "abcdef", expect(function (error) {
test.isFalse(error);
}));
},
logoutStep
]);
}) ();
@@ -778,16 +858,15 @@ if (Meteor.isServer) (function () {
// set a new password.
Accounts.setPassword(userId, 'new password');
user = Meteor.users.findOne(userId);
var oldVerifier = user.services.password.srp;
test.isTrue(user.services.password.srp);
var oldSaltedHash = user.services.password.bcrypt;
test.isTrue(oldSaltedHash);
// reset with the same password, see we get a different verifier
// reset with the same password, see we get a different salted hash
Accounts.setPassword(userId, 'new password');
user = Meteor.users.findOne(userId);
var newVerifier = user.services.password.srp;
test.notEqual(oldVerifier.salt, newVerifier.salt);
test.notEqual(oldVerifier.identity, newVerifier.identity);
test.notEqual(oldVerifier.verifier, newVerifier.verifier);
var newSaltedHash = user.services.password.bcrypt;
test.isTrue(newSaltedHash);
test.notEqual(oldSaltedHash, newSaltedHash);
// cleanup
Meteor.users.remove(userId);

View File

@@ -115,3 +115,39 @@ Meteor.methods({
Meteor.users.remove({ "username": username });
}
});
// Create a user that had previously logged in with SRP.
Meteor.methods({
testCreateSRPUser: function () {
var username = Random.id();
Meteor.users.remove({username: username});
var userId = Accounts.createUser({username: username});
Meteor.users.update(
userId,
{ '$set': { 'services.password.srp': {
"identity" : "iPNrshUEcpOSO5fRDu7o4RRDc9OJBCGGljYpcXCuyg9",
"salt" : "Dk3lFggdEtcHU3aKm6Odx7sdcaIrMskQxBbqtBtFzt6",
"verifier" : "2e8bce266b1357edf6952cc56d979db19f699ced97edfb2854b95972f820b0c7006c1a18e98aad40edf3fe111b87c52ef7dd06b320ce452d01376df2d560fdc4d8e74f7a97bca1f67b3cfaef34dee34dd6c76571c247d762624dc166dab5499da06bc9358528efa75bf74e2e7f5a80d09e60acf8856069ae5cfb080f2239ee76"
} } }
);
return username;
},
testSRPUpgrade: function (username) {
var user = Meteor.users.findOne({username: username});
if (user.services && user.services.password && user.services.password.srp)
throw new Error("srp wasn't removed");
if (!(user.services && user.services.password && user.services.password.bcrypt))
throw new Error("bcrypt wasn't added");
},
testNoSRPUpgrade: function (username) {
var user = Meteor.users.findOne({username: username});
if (user.services && user.services.password && user.services.password.bcrypt)
throw new Error("bcrypt was added");
if (user.services && user.services.password && ! user.services.password.srp)
throw new Error("srp was removed");
}
});

View File

@@ -48,46 +48,61 @@ AttributeHandler.extend = function (options) {
return subType;
};
// Extended below to support both regular and SVG elements
var BaseClassHandler = AttributeHandler.extend({
/// Apply the diff between the attributes of "oldValue" and "value" to "element."
//
// Each subclass must implement a parseValue method which takes a string
// as an input and returns a dict of attributes. The keys of the dict
// are unique identifiers (ie. css properties in the case of styles), and the
// values are the entire attribute which will be injected into the element.
//
// Extended below to support classes, SVG elements and styles.
var DiffingAttributeHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
if (!this.getCurrentValue || !this.setValue)
throw new Error("Missing methods in subclass of 'BaseClassHandler'");
if (!this.getCurrentValue || !this.setValue || !this.parseValue)
throw new Error("Missing methods in subclass of 'DiffingAttributeHandler'");
var oldClasses = oldValue ? _.compact(oldValue.split(' ')) : [];
var newClasses = value ? _.compact(value.split(' ')) : [];
var oldAttrsMap = oldValue ? this.parseValue(oldValue) : {};
var newAttrsMap = value ? this.parseValue(value) : {};
// the current classes on the element, which we will mutate.
var classes = _.compact(this.getCurrentValue(element).split(' '));
// the current attributes on the element, which we will mutate.
// optimize this later (to be asymptotically faster) if necessary
for (var i = 0; i < oldClasses.length; i++) {
var c = oldClasses[i];
if (! _.contains(newClasses, c))
classes = _.without(classes, c);
}
for (var i = 0; i < newClasses.length; i++) {
var c = newClasses[i];
if ((! _.contains(oldClasses, c)) &&
(! _.contains(classes, c)))
classes.push(c);
}
var attrString = this.getCurrentValue(element);
var attrsMap = attrString ? this.parseValue(attrString) : {};
this.setValue(element, classes.join(' '));
_.each(_.keys(oldAttrsMap), function (t) {
if (! (t in newAttrsMap))
delete attrsMap[t];
});
_.each(_.keys(newAttrsMap), function (t) {
attrsMap[t] = newAttrsMap[t];
});
this.setValue(element, _.values(attrsMap).join(' '));
}
});
var ClassHandler = BaseClassHandler.extend({
var ClassHandler = DiffingAttributeHandler.extend({
// @param rawValue {String}
getCurrentValue: function (element) {
return element.className;
},
setValue: function (element, className) {
element.className = className;
},
parseValue: function (attrString) {
var tokens = {};
_.each(attrString.split(' '), function(token) {
if (token)
tokens[token] = token;
});
return tokens;
}
});
var SVGClassHandler = BaseClassHandler.extend({
var SVGClassHandler = ClassHandler.extend({
getCurrentValue: function (element) {
return element.className.baseVal;
},
@@ -96,6 +111,46 @@ var SVGClassHandler = BaseClassHandler.extend({
}
});
var StyleHandler = DiffingAttributeHandler.extend({
getCurrentValue: function (element) {
return element.getAttribute('style');
},
setValue: function (element, style) {
if (style === '') {
element.removeAttribute('style');
} else {
element.setAttribute('style', style);
}
},
// Parse a string to produce a map from property to attribute string.
//
// Example:
// "color:red; foo:12px" produces a token {color: "color:red", foo:"foo:12px"}
parseValue: function (attrString) {
var tokens = {};
// Regex for parsing a css attribute declaration, taken from css-parse:
// https://github.com/reworkcss/css-parse/blob/7cef3658d0bba872cde05a85339034b187cb3397/index.js#L219
var regex = /(\*?[-#\/\*\\\w]+(?:\[[0-9a-z_-]+\])?)\s*:\s*(?:\'(?:\\\'|.)*?\'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+[;\s]*/g;
var match = regex.exec(attrString);
while (match) {
// match[0] = entire matching string
// match[1] = css property
// Prefix the token to prevent conflicts with existing properties.
// XXX No `String.trim` on Safari 4. Swap out $.trim if we want to
// remove strong dep on jquery.
tokens[' ' + match[1]] = match[0].trim ?
match[0].trim() : $.trim(match[0]);
match = regex.exec(attrString);
}
return tokens;
}
});
var BooleanHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
var name = this.name;
@@ -178,24 +233,20 @@ if (Meteor.isClient) {
var anchorForNormalization = document.createElement('A');
}
var normalizeUrl = function (url) {
var getUrlProtocol = function (url) {
if (Meteor.isClient) {
anchorForNormalization.href = url;
return anchorForNormalization.href;
return (anchorForNormalization.protocol || "").toLowerCase();
} else {
throw new Error('normalizeUrl not implemented on the server');
throw new Error('getUrlProtocol not implemented on the server');
}
};
// UrlHandler is an attribute handler for all HTML attributes that take
// URL values. It disallows javascript: URLs, unless
// UI._allowJavascriptUrls() has been called. To detect javascript:
// urls, we set the attribute and then reads the attribute out of the
// DOM, in order to avoid writing our own URL normalization code. (We
// don't want to be fooled by ' javascript:alert(1)' or
// 'jAvAsCrIpT:alert(1)'.) In future, when the URL interface is more
// widely supported, we can use that, which will be
// cleaner. https://developer.mozilla.org/en-US/docs/Web/API/URL
// urls, we set the attribute on a dummy anchor element and then read
// out the 'protocol' property of the attribute.
var origUpdate = AttributeHandler.prototype.update;
var UrlHandler = AttributeHandler.extend({
update: function (element, oldValue, value) {
@@ -205,8 +256,7 @@ var UrlHandler = AttributeHandler.extend({
if (UI._javascriptUrlsAllowed()) {
origUpdate.apply(self, args);
} else {
var isJavascriptProtocol =
(normalizeUrl(value).indexOf('javascript:') === 0);
var isJavascriptProtocol = (getUrlProtocol(value) === "javascript:");
if (isJavascriptProtocol) {
Meteor._debug("URLs that use the 'javascript:' protocol are not " +
"allowed in URL attribute values. " +
@@ -230,6 +280,8 @@ makeAttributeHandler = function (elem, name, value) {
} else {
return new ClassHandler(name, value);
}
} else if (name === 'style') {
return new StyleHandler(name, value);
} else if ((elem.tagName === 'OPTION' && name === 'selected') ||
(elem.tagName === 'INPUT' && name === 'checked')) {
return new BooleanHandler(name, value);

View File

@@ -1,13 +1,16 @@
{
"dependencies": {
"less": {
"version": "1.6.1",
"version": "1.7.1",
"dependencies": {
"graceful-fs": {
"version": "2.0.3"
},
"mime": {
"version": "1.2.11"
},
"request": {
"version": "2.33.0",
"version": "2.34.0",
"dependencies": {
"qs": {
"version": "0.6.6"
@@ -16,7 +19,7 @@
"version": "5.0.0"
},
"forever-agent": {
"version": "0.5.0"
"version": "0.5.2"
},
"node-uuid": {
"version": "1.4.1"
@@ -25,12 +28,12 @@
"version": "0.12.1",
"dependencies": {
"punycode": {
"version": "1.2.3"
"version": "1.2.4"
}
}
},
"form-data": {
"version": "0.1.2",
"version": "0.1.3",
"dependencies": {
"combined-stream": {
"version": "0.0.4",
@@ -41,7 +44,7 @@
}
},
"async": {
"version": "0.2.10"
"version": "0.9.0"
}
}
},
@@ -91,15 +94,15 @@
"version": "0.3.5"
},
"clean-css": {
"version": "2.0.7",
"version": "2.1.8",
"dependencies": {
"commander": {
"version": "2.0.0"
"version": "2.1.0"
}
}
},
"source-map": {
"version": "0.1.31",
"version": "0.1.34",
"dependencies": {
"amdefine": {
"version": "0.1.0"

View File

@@ -8,12 +8,12 @@ Package._transitional_registerBuildPlugin({
sources: [
'plugin/compile-less.js'
],
npmDependencies: {"less": "1.6.1"}
npmDependencies: {"less": "1.7.1"}
});
Package.on_test(function (api) {
api.use(['test-helpers', 'tinytest', 'less', 'templating']);
api.add_files(['less_tests.less', 'less_tests.js', 'less_tests.html',
'less_tests.import.less', 'less_tests_empty.less'],
'less_tests_empty.less'],
'client');
});

View File

@@ -44,7 +44,7 @@ Plugin.registerSourceHandler("less", function (compileStep) {
compileStep.error({
message: "Less compiler error: " + e.message,
sourcePath: e.filename || compileStep.inputPath,
line: e.line - 1, // dunno why, but it matches
line: e.line,
column: e.column + 1
});
return;

View File

@@ -784,8 +784,12 @@ _.extend(Connection.prototype, {
if (Meteor.isClient) {
// On the client, we don't have fibers, so we can't block. The
// only thing we can do is to return undefined and discard the
// result of the RPC.
callback = function () {};
// result of the RPC. If an error occurred then print the error
// to the console.
callback = function (err) {
err && Meteor._debug("Error invoking Method '" + name + "':",
err.message);
};
} else {
// On the server, make the function synchronous. Throw on
// errors, return on success.

View File

@@ -4,7 +4,13 @@ MeteorDeveloperAccounts = {};
// @param credentialRequestCompleteCallback {Function} Callback function to call on
// completion. Takes one argument, credentialToken on success, or Error on
// error.
var requestCredential = function (credentialRequestCompleteCallback) {
var requestCredential = function (options, credentialRequestCompleteCallback) {
// support a callback without options
if (! credentialRequestCompleteCallback && typeof options === "function") {
credentialRequestCompleteCallback = options;
options = null;
}
var config = ServiceConfiguration.configurations.findOne({
service: 'meteor-developer'
});
@@ -20,8 +26,12 @@ var requestCredential = function (credentialRequestCompleteCallback) {
METEOR_DEVELOPER_URL + "/oauth2/authorize?" +
"state=" + credentialToken +
"&response_type=code&" +
"client_id=" + config.clientId +
"&redirect_uri=" + Meteor.absoluteUrl("_oauth/meteor-developer?close");
"client_id=" + config.clientId;
if (options && options.userEmail)
loginUrl += '&user_email=' + encodeURIComponent(options.userEmail);
loginUrl += "&redirect_uri=" + Meteor.absoluteUrl("_oauth/meteor-developer?close");
OAuth.showPopup(
loginUrl,

View File

@@ -75,3 +75,12 @@ Tinytest.add("environment - helpers", function (test) {
Meteor._delete(x, "a");
test.equal(x, {});
});
Tinytest.add("environment - startup", function (test) {
// After startup, Meteor.startup should call the callback immediately.
var called = false;
Meteor.startup(function () {
called = true;
});
test.isTrue(called);
});

View File

@@ -1,3 +1,8 @@
Meteor.startup = function (callback) {
__meteor_bootstrap__.startup_hooks.push(callback);
if (__meteor_bootstrap__.startupHooks) {
__meteor_bootstrap__.startupHooks.push(callback);
} else {
// We already started up. Just call it now.
callback();
}
};

View File

@@ -116,7 +116,8 @@ ObserveSequence = {
var idString = idStringify(id);
if (idsUsed[idString]) {
warn("duplicate id " + id + " in", seq);
if (typeof item === 'object' && '_id' in item)
warn("duplicate id " + id + " in", seq);
id = Random.id();
} else {
idsUsed[idString] = true;

View File

@@ -483,7 +483,7 @@ Tinytest.add('observe-sequence - number arrays', function (test) {
{removedAt: [{NOT: 1}, 1, 1]},
{addedAt: [3, 3, 1, 2]},
{addedAt: [{NOT: 3}, 3, 3, null]}
], /*numExpectedWarnings = */2);
]);
});
Tinytest.add('observe-sequence - cursor to other cursor, same collection', function (test) {

1
packages/sha/.gitignore vendored Normal file
View File

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

9
packages/sha/package.js Normal file
View File

@@ -0,0 +1,9 @@
Package.describe({
summary: "SHA256 implementation",
internal: true
});
Package.on_use(function (api) {
api.export('SHA256');
api.add_files(['sha256.js'], ['client', 'server']);
});

View File

@@ -1,7 +1,5 @@
/// METEOR WRAPPER
//
// XXX this should get packaged and moved into the Meteor.crypto
// namespace, along with other hash functions.
SHA256 = (function () {
@@ -14,18 +12,18 @@ SHA256 = (function () {
* Original code by Angel Marin, Paul Johnston.
*
**/
function SHA256(s){
var chrsz = 8;
var hexcase = 0;
function safe_add (x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
function R (X, n) { return ( X >>> n ); }
function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
@@ -34,17 +32,17 @@ function SHA256(s){
function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
function core_sha256 (m, l) {
var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
var W = new Array(64);
var a, b, c, d, e, f, g, h, i, j;
var T1, T2;
m[l >> 5] |= 0x80 << (24 - l % 32);
m[((l + 64 >> 9) << 4) + 15] = l;
for ( var i = 0; i<m.length; i+=16 ) {
a = HASH[0];
b = HASH[1];
@@ -54,14 +52,14 @@ function SHA256(s){
f = HASH[5];
g = HASH[6];
h = HASH[7];
for ( var j = 0; j<64; j++) {
if (j < 16) W[j] = m[j + i];
else W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
T2 = safe_add(Sigma0256(a), Maj(a, b, c));
h = g;
g = f;
f = e;
@@ -71,7 +69,7 @@ function SHA256(s){
b = a;
a = safe_add(T1, T2);
}
HASH[0] = safe_add(a, HASH[0]);
HASH[1] = safe_add(b, HASH[1]);
HASH[2] = safe_add(c, HASH[2]);
@@ -83,7 +81,7 @@ function SHA256(s){
}
return HASH;
}
function str2binb (str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
@@ -92,7 +90,7 @@ function SHA256(s){
}
return bin;
}
function Utf8Encode(string) {
// METEOR change:
// The webtoolkit.info version of this code added this
@@ -102,11 +100,11 @@ function SHA256(s){
//
// string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
@@ -119,12 +117,12 @@ function SHA256(s){
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
}
function binb2hex (binarray) {
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
@@ -134,10 +132,10 @@ function SHA256(s){
}
return str;
}
s = Utf8Encode(s);
return binb2hex(core_sha256(str2binb(s), s.length * chrsz));
}
/// METEOR WRAPPER

View File

@@ -656,6 +656,34 @@ Hi there!
<a href="#bad-url" id="spacebars_test_event_returns_false_link">click me</a>
</template>
<template name="spacebars_test_event_selectors1">
<div>{{> spacebars_test_event_selectors2}}</div>
</template>
<template name="spacebars_test_event_selectors2">
<p class="p1">Not it</p>
<div><p class="p2">It</p></div>
</template>
<template name="spacebars_test_event_selectors_capturing1">
<div>{{> spacebars_test_event_selectors_capturing2}}</div>
</template>
<template name="spacebars_test_event_selectors_capturing2">
<video class='video1'>
<source id='mp4'
src="http://media.w3.org/2010/05/sintel/trailer.mp4"
type='video/mp4'>
</video>
<div>
<video class='video2'>
<source id='mp4'
src="http://media.w3.org/2010/05/sintel/trailer.mp4"
type='video/mp4'>
</video>
</div>
</template>
<template name="spacebars_test_tables1">
<table><tr><td>Foo</td></tr></table>
</template>
@@ -752,8 +780,22 @@ Hi there!
</div>
</template>
<template name="spacebars_test_ui_hooks_nested">
{{#if foo}}
{{> spacebars_test_ui_hooks_nested_sub}}
{{/if}}
</template>
<template name="spacebars_test_ui_hooks_nested_sub">
<div>
{{#with true}}
<p>hello</p>
{{/with}}
</div>
</template>
<template name="spacebars_test_template_instance_helper">
{{foo}}
{{#with true}}{{foo}}{{/with}}
</template>
<template name="spacebars_test_with_cleanup">
@@ -763,3 +805,21 @@ Hi there!
{{/with}}
</div>
</template>
<template name="spacebars_test_template_parent_data_helper">
{{#with "parent"}}
{{> spacebars_test_template_parent_data_helper_child}}
{{/with}}
</template>
<template name="spacebars_test_template_parent_data_helper_child">
{{#each a}}
{{#with b}}
{{#if c}}
{{#with "d"}}
{{foo}}
{{/with}}
{{/if}}
{{/with}}
{{/each}}
</template>

View File

@@ -1721,6 +1721,60 @@ Tinytest.add("spacebars-tests - template_tests - event handler returns false",
}
);
// Make sure that if you bind an event on "div p", for example,
// both the div and the p need to be in the template. jQuery's
// `$(elem).find(...)` works this way, but the browser's
// querySelector doesn't.
Tinytest.add(
"spacebars-tests - template_tests - event map selector scope",
function (test) {
var tmpl = Template.spacebars_test_event_selectors1;
var tmpl2 = Template.spacebars_test_event_selectors2;
var buf = [];
tmpl2.events({
'click div p': function (evt) { buf.push(evt.currentTarget.className); }
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
test.equal(buf.join(), '');
clickIt(div.querySelector('.p1'));
test.equal(buf.join(), '');
clickIt(div.querySelector('.p2'));
test.equal(buf.join(), 'p2');
document.body.removeChild(div);
}
);
if (document.addEventListener) {
// see note about non-bubbling events in the "capuring events"
// templating test for why we use the VIDEO tag. (It would be
// nice to get rid of the network dependency, though.)
// We skip this test in IE 8.
Tinytest.add(
"spacebars-tests - template_tests - event map selector scope (capturing)",
function (test) {
var tmpl = Template.spacebars_test_event_selectors_capturing1;
var tmpl2 = Template.spacebars_test_event_selectors_capturing2;
var buf = [];
tmpl2.events({
'play div video': function (evt) { buf.push(evt.currentTarget.className); }
});
var div = renderToDiv(tmpl);
document.body.appendChild(div);
test.equal(buf.join(), '');
simulateEvent(div.querySelector(".video1"),
"play", {}, {bubbles: false});
test.equal(buf.join(), '');
simulateEvent(div.querySelector(".video2"),
"play", {}, {bubbles: false});
test.equal(buf.join(), 'video2');
document.body.removeChild(div);
}
);
}
Tinytest.add("spacebars-tests - template_tests - tables", function (test) {
var tmpl1 = Template.spacebars_test_tables1;
@@ -2047,6 +2101,40 @@ Tinytest.add(
}
);
Tinytest.add(
"spacebars-tests - template_tests - ui hooks - nested domranges",
function (test) {
var tmpl = Template.spacebars_test_ui_hooks_nested;
var rv = new ReactiveVar(true);
tmpl.foo = function () {
return rv.get();
};
var subtmpl = Template.spacebars_test_ui_hooks_nested_sub;
var uiHookCalled = false;
subtmpl.rendered = function () {
this.firstNode.parentNode._uihooks = {
removeElement: function (node) {
uiHookCalled = true;
}
};
};
var div = renderToDiv(tmpl);
document.body.appendChild(div);
Deps.flush();
var htmlBeforeRemove = canonicalizeHtml(div.innerHTML);
rv.set(false);
Deps.flush();
test.isTrue(uiHookCalled);
var htmlAfterRemove = canonicalizeHtml(div.innerHTML);
test.equal(htmlBeforeRemove, htmlAfterRemove);
document.body.removeChild(div);
}
);
Tinytest.add(
"spacebars-tests - template_tests - access template instance from helper",
function (test) {
@@ -2121,3 +2209,42 @@ Tinytest.add(
test.equal(helperCalled, false);
}
);
Tinytest.add(
"spacebars-tests - template_tests - access parent data contexts from helper",
function (test) {
var childTmpl = Template.spacebars_test_template_parent_data_helper_child;
var parentTmpl = Template.spacebars_test_template_parent_data_helper;
var rv = new ReactiveVar(0);
childTmpl.a = ["a"];
childTmpl.b = new ReactiveVar("b");
childTmpl.c = ["c"];
childTmpl.foo = function () {
var data = UI._parentData(rv.get());
return data.get === undefined ? data : data.get();
};
var div = renderToDiv(parentTmpl);
test.equal(canonicalizeHtml(div.innerHTML), "d");
rv.set(1);
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), "b");
// Test UI._parentData() reactivity
childTmpl.b.set("bNew");
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), "bNew");
rv.set(2);
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), "a");
rv.set(3);
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), "parent");
}
);

View File

@@ -1,13 +1,18 @@
// XXX COMPAT WITH 0.8.1.3
// This package is replaced by the use of bcrypt in accounts-password,
// but we are leaving in some of the code to allow existing user
// databases to be upgraded from SRP to bcrypt.
Package.describe({
summary: "Library for Secure Remote Password (SRP) exchanges",
internal: true
});
Package.on_use(function (api) {
api.use(['random', 'check'], ['client', 'server']);
api.use(['random', 'check', 'sha'], ['client', 'server']);
api.use('underscore');
api.export('SRP');
api.add_files(['biginteger.js', 'sha256.js', 'srp.js'],
api.add_files(['biginteger.js', 'srp.js'],
['client', 'server']);
});

View File

@@ -1,6 +1,11 @@
SRP = {};
// This package contains just enough of the original SRP code to
// support the backwards-compatibility upgrade path.
//
// An SRP (and possibly also accounts-srp) package should eventually be
// available in Atmosphere so that users can continue to use SRP if they
// want to.
/////// PUBLIC CLIENT
SRP = {};
/**
* Generate a new SRP verifier. Password is the plaintext password.
@@ -8,6 +13,7 @@ SRP = {};
* options is optional and can include:
* - identity: String. The SRP username to user. Mostly this is passed
* in for testing. Random UUID if not provided.
* - hashedIdentityAndPassword: combined identity and password, already hashed, for the SRP to bcrypt upgrade path.
* - salt: String. A salt to use. Mostly this is passed in for
* testing. Random UUID if not provided.
* - SRP parameters (see _defaults and paramsFromOptions below)
@@ -15,14 +21,19 @@ SRP = {};
SRP.generateVerifier = function (password, options) {
var params = paramsFromOptions(options);
var identity = (options && options.identity) || Random.secret();
var salt = (options && options.salt) || Random.secret();
var x = params.hash(salt + params.hash(identity + ":" + password));
var identity;
var hashedIdentityAndPassword = options && options.hashedIdentityAndPassword;
if (!hashedIdentityAndPassword) {
identity = (options && options.identity) || Random.secret();
hashedIdentityAndPassword = params.hash(identity + ":" + password);
}
var x = params.hash(salt + hashedIdentityAndPassword);
var xi = new BigInteger(x, 16);
var v = params.g.modPow(xi, params.N);
return {
identity: identity,
salt: salt,
@@ -38,249 +49,6 @@ SRP.matchVerifier = {
};
/**
* Generate a new SRP client object. Password is the plaintext password.
*
* options is optional and can include:
* - a: client's private ephemeral value. String or
* BigInteger. Normally, this is picked randomly, but it can be
* passed in for testing.
* - SRP parameters (see _defaults and paramsFromOptions below)
*/
SRP.Client = function (password, options) {
var self = this;
self.params = paramsFromOptions(options);
self.password = password;
// shorthand
var N = self.params.N;
var g = self.params.g;
// construct public and private keys.
var a, A;
if (options && options.a) {
if (typeof options.a === "string")
a = new BigInteger(options.a, 16);
else if (options.a instanceof BigInteger)
a = options.a;
else
throw new Error("Invalid parameter: a");
A = g.modPow(a, N);
if (A.mod(N) === 0)
throw new Error("Invalid parameter: a: A mod N == 0.");
} else {
while (!A || A.mod(N) === 0) {
a = randInt();
A = g.modPow(a, N);
}
}
self.a = a;
self.A = A;
self.Astr = A.toString(16);
};
/**
* Initiate an SRP exchange.
*
* returns { A: 'client public ephemeral key. hex encoded integer.' }
*/
SRP.Client.prototype.startExchange = function () {
var self = this;
return {
A: self.Astr
};
};
/**
* Respond to the server's challenge with a proof of password.
*
* challenge is an object with
* - B: server public ephemeral key. hex encoded integer.
* - identity: user's identity (SRP username).
* - salt: user's salt.
*
* returns { M: 'client proof of password. hex encoded integer.' }
* throws an error if it got an invalid challenge.
*/
SRP.Client.prototype.respondToChallenge = function (challenge) {
var self = this;
// shorthand
var N = self.params.N;
var g = self.params.g;
var k = self.params.k;
var H = self.params.hash;
// XXX check for missing / bad parameters.
self.identity = challenge.identity;
self.salt = challenge.salt;
self.Bstr = challenge.B;
self.B = new BigInteger(self.Bstr, 16);
if (self.B.mod(N) === 0)
throw new Error("Server sent invalid key: B mod N == 0.");
var u = new BigInteger(H(self.Astr + self.Bstr), 16);
var x = new BigInteger(
H(self.salt + H(self.identity + ":" + self.password)), 16);
var kgx = k.multiply(g.modPow(x, N));
var aux = self.a.add(u.multiply(x));
var S = self.B.subtract(kgx).modPow(aux, N);
var M = H(self.Astr + self.Bstr + S.toString(16));
var HAMK = H(self.Astr + M + S.toString(16));
self.S = S;
self.HAMK = HAMK;
return {
M: M
};
};
/**
* Verify server's confirmation message.
*
* confirmation is an object with
* - HAMK: server's proof of password.
*
* returns true or false.
*/
SRP.Client.prototype.verifyConfirmation = function (confirmation) {
var self = this;
return (self.HAMK && (confirmation.HAMK === self.HAMK));
};
/////// PUBLIC SERVER
/**
* Generate a new SRP server object.
*
* options is optional and can include:
* - b: server's private ephemeral value. String or
* BigInteger. Normally, this is picked randomly, but it can be
* passed in for testing.
* - SRP parameters (see _defaults and paramsFromOptions below)
*/
SRP.Server = function (verifier, options) {
var self = this;
self.params = paramsFromOptions(options);
self.verifier = verifier;
// shorthand
var N = self.params.N;
var g = self.params.g;
var k = self.params.k;
var v = new BigInteger(self.verifier.verifier, 16);
// construct public and private keys.
var b, B;
if (options && options.b) {
if (typeof options.b === "string")
b = new BigInteger(options.b, 16);
else if (options.b instanceof BigInteger)
b = options.b;
else
throw new Error("Invalid parameter: b");
B = k.multiply(v).add(g.modPow(b, N)).mod(N);
if (B.mod(N) === 0)
throw new Error("Invalid parameter: b: B mod N == 0.");
} else {
while (!B || B.mod(N) === 0) {
b = randInt();
B = k.multiply(v).add(g.modPow(b, N)).mod(N);
}
}
self.b = b;
self.B = B;
self.Bstr = B.toString(16);
};
/**
* Issue a challenge to the client.
*
* Takes a request from the client containing:
* - A: hex encoded int.
*
* Returns a challenge with:
* - B: server public ephemeral key. hex encoded integer.
* - identity: user's identity (SRP username).
* - salt: user's salt.
*
* Throws an error if issued a bad request.
*/
SRP.Server.prototype.issueChallenge = function (request) {
var self = this;
// XXX check for missing / bad parameters.
self.Astr = request.A;
self.A = new BigInteger(self.Astr, 16);
if (self.A.mod(self.params.N) === 0)
throw new Error("Client sent invalid key: A mod N == 0.");
// shorthand
var N = self.params.N;
var H = self.params.hash;
// Compute M and HAMK in advance. Don't send to client yet.
var u = new BigInteger(H(self.Astr + self.Bstr), 16);
var v = new BigInteger(self.verifier.verifier, 16);
var avu = self.A.multiply(v.modPow(u, N));
self.S = avu.modPow(self.b, N);
self.M = H(self.Astr + self.Bstr + self.S.toString(16));
self.HAMK = H(self.Astr + self.M + self.S.toString(16));
return {
identity: self.verifier.identity,
salt: self.verifier.salt,
B: self.Bstr
};
};
/**
* Verify a response from the client and return confirmation.
*
* Takes a challenge response from the client containing:
* - M: client proof of password. hex encoded int.
*
* Returns a confirmation if the client's proof is good:
* - HAMK: server proof of password. hex encoded integer.
* OR null if the client's proof doesn't match.
*/
SRP.Server.prototype.verifyResponse = function (response) {
var self = this;
if (response.M !== self.M)
return null;
return {
HAMK: self.HAMK
};
};
/////// INTERNAL
/**
* Default parameter values for SRP.
*
@@ -331,8 +99,3 @@ var paramsFromOptions = function (options) {
return ret;
};
var randInt = function () {
return new BigInteger(Random.hexString(36), 16);
};

View File

@@ -1,38 +1,6 @@
Tinytest.add("srp - good exchange", function(test) {
var password = 'hi there!';
var verifier = SRP.generateVerifier(password);
var C = new SRP.Client(password);
var S = new SRP.Server(verifier);
var request = C.startExchange();
var challenge = S.issueChallenge(request);
var response = C.respondToChallenge(challenge);
var confirmation = S.verifyResponse(response);
test.isTrue(confirmation);
test.isTrue(C.verifyConfirmation(confirmation));
});
Tinytest.add("srp - bad exchange", function(test) {
var verifier = SRP.generateVerifier('one password');
var C = new SRP.Client('another password');
var S = new SRP.Server(verifier);
var request = C.startExchange();
var challenge = S.issueChallenge(request);
var response = C.respondToChallenge(challenge);
var confirmation = S.verifyResponse(response);
test.isFalse(confirmation);
});
Tinytest.add("srp - fixed values", function(test) {
// Test exact values during the exchange. We have to be very careful
// about changing the SRP code, because changes could render
// Test exact values outputted by `generateVerifier`. We have to be very
// careful about changing the SRP code, because changes could render
// people's existing user database unusable. This test is
// intentionally brittle to catch change that could affect the
// validity of user passwords.
@@ -45,71 +13,7 @@ Tinytest.add("srp - fixed values", function(test) {
var verifier = SRP.generateVerifier(
password, {identity: identity, salt: salt});
var C = new SRP.Client(password, {a: a});
var S = new SRP.Server(verifier, {b: b});
var request = C.startExchange();
test.equal(request.A, "8a75aa61471a92d4c3b5d53698c910af5ef013c42799876c40612d1d5e0dc41d01f669bc022fadcd8a704030483401a1b86b8670191bd9dfb1fb506dd11c688b2f08e9946756263954db2040c1df1894af7af5f839c9215bb445268439157e65e8f100469d575d5d0458e19e8bd4dd4ea2c0b30b1b3f4f39264de4ec596e0bb7");
var challenge = S.issueChallenge(request);
test.equal(challenge.B, "77ab0a40ef428aa2fa2bc257c905f352c7f75fbcfdb8761393c9dc0f730bbb0270ba9f837545b410c955c3f761494b329ad23c6efdec7e63509e538c2f68a3526e072550a11dac46017718362205e0c698b5bed67d6ff475aa92c191ca169f865c81a1a577373c449b98df720c7b7ff50536f9919d781e698025fd7164932ba7");
var response = C.respondToChallenge(challenge);
test.equal(response.M, "8705d31bb61497279adf44eef6c167dcb7e03aa7a42102c1ea7e73025fbd4cd9");
var confirmation = S.verifyResponse(response);
test.equal(confirmation.HAMK, "07a0f200392fa9a084db7acc2021fbc174bfb36956b46835cc12506b68b27bba");
test.isTrue(C.verifyConfirmation(confirmation));
});
Tinytest.add("srp - options", function(test) {
// test that all options are respected.
//
// Note, all test strings here should be hex, because the 'hash'
// function needs to output numbers.
var baseOptions = {
hash: function (x) { return x; },
N: 'b',
g: '2',
k: '1'
};
var verifierOptions = _.extend({
identity: 'a',
salt: 'b'
}, baseOptions);
var clientOptions = _.extend({
a: "2"
}, baseOptions);
var serverOptions = _.extend({
b: "2"
}, baseOptions);
var verifier = SRP.generateVerifier('c', verifierOptions);;
test.equal(verifier.identity, 'a');
test.equal(verifier.salt, 'b');
test.equal(verifier.verifier, '3');
var C = new SRP.Client('c', clientOptions);
var S = new SRP.Server(verifier, serverOptions);
var request = C.startExchange();
test.equal(request.A, '4');
var challenge = S.issueChallenge(request);
test.equal(challenge.identity, 'a');
test.equal(challenge.salt, 'b');
test.equal(challenge.B, '7');
var response = C.respondToChallenge(challenge);
test.equal(response.M, '471');
var confirmation = S.verifyResponse(response);
test.isTrue(confirmation);
test.equal(confirmation.HAMK, '44711');
test.equal(verifier.identity, identity);
test.equal(verifier.salt, salt);
test.equal(verifier.verifier, "56778b720d20b2e306f04e47180fb94335b88a6052808483acb0e85612606f9f1d8d5a3c6b85e0c7bfec7f08c07bdfbd0d40b032f517871dd8afd045b0f24e2edc05ccdc47b19f35d2eb9f7670521a38c1b358fcee63f052a1aedbb1282d3b92c7a554f8523f3379c2fbc6885be8227fbd426ad6960c3839809f8c94d80a6c51");
});

View File

@@ -23,7 +23,7 @@
}
},
"stylus": {
"version": "0.42.3",
"version": "0.46.3",
"dependencies": {
"css-parse": {
"version": "1.7.0"
@@ -32,16 +32,24 @@
"version": "0.3.5"
},
"debug": {
"version": "0.7.4"
"version": "1.0.1",
"dependencies": {
"ms": {
"version": "0.6.2"
}
}
},
"sax": {
"version": "0.5.8"
},
"glob": {
"version": "3.2.9",
"version": "3.2.11",
"dependencies": {
"inherits": {
"version": "2.0.1"
},
"minimatch": {
"version": "0.2.14",
"version": "0.3.0",
"dependencies": {
"lru-cache": {
"version": "2.5.0"
@@ -50,9 +58,6 @@
"version": "1.0.0"
}
}
},
"inherits": {
"version": "2.0.1"
}
}
}

View File

@@ -8,7 +8,7 @@ Package._transitional_registerBuildPlugin({
sources: [
'plugin/compile-stylus.js'
],
npmDependencies: { stylus: "0.42.3", nib: "1.0.2" }
npmDependencies: { stylus: "0.46.3", nib: "1.0.2" }
});
Package.on_test(function (api) {

View File

@@ -31,12 +31,15 @@ canonicalizeHtml = function(html) {
attrs = attrs.replace(/\s+/g, ' ');
// quote unquoted attribute values, as in `type=checkbox`. This
// will do the wrong thing if there's an `=` in an attribute value.
attrs = attrs.replace(/(\w)=([^" >/]+)/g, '$1="$2"');
// for the purpose of splitting attributes in a string like
// 'a="b" c="d"', assume they are separated by a single space
// and values are double-quoted, but allow for spaces inside
// the quotes. Split on space following quote.
var attrList = attrs.replace(/" /g, '"\u0000').split('\u0000');
attrs = attrs.replace(/(\w)=([^'" >/]+)/g, '$1="$2"');
// for the purpose of splitting attributes in a string like 'a="b"
// c="d"', assume they are separated by a single space and values
// are double- or single-quoted, but allow for spaces inside the
// quotes. Split on space following quote.
var attrList = attrs.replace(/(\w)='([^']*)' /g, "$1='$2'\u0000");
attrList = attrList.replace(/(\w)="([^"]*)" /g, '$1="$2"\u0000');
attrList = attrList.split("\u0000");
// put attributes in alphabetical order
attrList.sort();
@@ -59,11 +62,33 @@ canonicalizeHtml = function(html) {
if (key === 'sizset')
continue;
var value = a[1];
value = value.replace(/["'`]/g, '"');
// this check is probably made unreachable by a regex above
// that quotes unquoted attribute values
if (value.charAt(0) !== '"')
value = '"'+value+'"';
// make sure the attribute is doubled-quoted
if (value.charAt(0) === '"') {
// Do nothing
} else {
if (value.charAt(0) !== "'") {
// attribute is unquoted. should be unreachable because of
// regex above.
value = '"' + value + '"';
} else {
// attribute is single-quoted. make it double-quoted.
value = value.replace(/\"/g, "&quot;");
}
value = value.replace(/["'`]/g, '"');
}
// Encode quotes and double quotes in the attribute.
var attr = value.slice(1, -1);
attr = attr.replace(/\"/g, "&quot;");
attr = attr.replace(/\'/g, "&quot;");
value = '"' + attr + '"';
// Ensure that styles do not end with a semicolon.
if (key === 'style') {
value = value.replace(/;\"$/, '"');
}
tagContents.push(key+'='+value);
}
return '<'+tagContents.join(' ')+'>';

View File

@@ -6,6 +6,9 @@ Package.describe({
Package.on_use(function (api) {
api.export(['UI', 'Handlebars']);
api.use('jquery'); // should be a weak dep, by having multiple "DOM backends"
// XXX StyleHandler uses $.trim since Safari 4 doesn't support
// `String.trim`. We should just replace this with our own `trim` if
// we want to make jquery a weak dep.
api.use('deps');
api.use('random');
api.use('ejson');

View File

@@ -214,6 +214,15 @@ Tinytest.add("ui - render - isolates", function (test) {
});
// IE strips malformed styles like "bar::d" from the `style`
// attribute. We detect this to adjust expectations for the StyleHandler
// test below.
var malformedStylesAllowed = function () {
var div = document.createElement("div");
div.setAttribute("style", "bar::d;");
return (div.getAttribute("style") === "bar::d;");
};
Tinytest.add("ui - render - isolate GC", function (test) {
// test that removing parent element removes listeners and stops autoruns.
(function () {
@@ -282,6 +291,87 @@ Tinytest.add("ui - render - reactive attributes", function (test) {
test.equal(R.numListeners(), 0);
})();
// Test styles.
(function () {
// Test the case where there is a semicolon in the css attribute.
var R = ReactiveVar({'style': 'foo: "a;aa"; bar: b;',
id: 'foo'});
var spanCode = SPAN({$dynamic: [function () { return R.get(); }]});
test.equal(toHTML(spanCode), '<span style="foo: &quot;a;aa&quot;; bar: b;" id="foo"></span>');
test.equal(R.numListeners(), 0);
var div = document.createElement("DIV");
materialize(spanCode, div);
test.equal(canonicalizeHtml(div.innerHTML), '<span id="foo" style="foo: &quot;a;aa&quot;; bar: b"></span>');
test.equal(R.numListeners(), 1);
var span = div.firstChild;
test.equal(span.nodeName, 'SPAN');
span.setAttribute('style', span.getAttribute('style') + '; jquery-style: hidden');
R.set({'style': 'foo: "a;zz;aa";', id: 'bar'});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML, true), '<span id="bar" style="foo: &quot;a;zz;aa&quot;; jquery-style: hidden"></span>');
test.equal(R.numListeners(), 1);
R.set({});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span style="jquery-style: hidden"></span>');
test.equal(R.numListeners(), 1);
$(div).remove();
test.equal(R.numListeners(), 0);
})();
// Test that identical styles are successfully overwritten.
(function () {
var R = ReactiveVar({'style': 'foo: a;'});
var spanCode = SPAN({$dynamic: [function () { return R.get(); }]});
var div = document.createElement("DIV");
document.body.appendChild(div);
materialize(spanCode, div);
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: a"></span>');
var span = div.firstChild;
test.equal(span.nodeName, 'SPAN');
span.setAttribute("style", 'foo: b;');
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: b"></span>');
R.set({'style': 'foo: c;'});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c"></span>');
// test malformed styles - different expectations in IE (which
// strips malformed styles) from other browsers
R.set({'style': 'foo: a; bar::d;:e; baz: c;'});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML),
malformedStylesAllowed() ?
'<span style="foo: a; bar::d; baz: c"></span>' :
'<span style="foo: a; baz: c"></span>');
// Test strange styles
R.set({'style': ' foo: c; constructor: a; __proto__: b;'});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: c; constructor: a; __proto__: b"></span>');
R.set({});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span></span>');
R.set({'style': 'foo: bar;'});
Deps.flush();
test.equal(canonicalizeHtml(div.innerHTML), '<span style="foo: bar"></span>');
})();
// Test `null`, `undefined`, and `[]` attributes
(function () {
var R = ReactiveVar({id: 'foo',
@@ -566,6 +656,35 @@ Tinytest.add("ui - UI.render", function (test) {
document.body.removeChild(div);
});
Tinytest.add("ui - UI.insert fails on jQuery objects", function (test) {
var tmpl = UI.Component.extend({
render: function () {
return SPAN();
}
});
test.throws(function () {
UI.insert(UI.render(tmpl), $('body'));
}, /'parentElement' must be a DOM node/);
test.throws(function () {
UI.insert(UI.render(tmpl), document.body, $('body'));
}, /'nextNode' must be a DOM node/);
});
Tinytest.add("ui - UI.getDataContext", function (test) {
var div = document.createElement("DIV");
var tmpl = UI.Component.extend({
render: function () {
return SPAN();
}
});
UI.insert(UI.renderWithData(tmpl, {foo: "bar"}), div);
var span = $(div).children('SPAN')[0];
test.isTrue(span);
test.equal(UI.getElementData(span), {foo: "bar"});
});
Tinytest.add("ui - UI.render _nestInCurrentComputation flag", function (test) {
_.each([true, false], function (nest) {

View File

@@ -1,5 +1,7 @@
=> Meteor 0.8.1.3: Fixes a security flaw in the `spiderable` package and
minor regressions from 0.8.1.
=> Meteor 0.8.2: Switch `accounts-password` to use bcrypt on the
server. User accounts will seamlessly transition to bcrypt on the
next login, but this transition is one-way, so you cannot downgrade a
production app once you upgrade to 0.8.2.
This release is being downloaded in the background. Update your
project to Meteor 0.8.1.3 by running 'meteor update'.
project to Meteor 0.8.2 by running 'meteor update'.

View File

@@ -1,12 +0,0 @@
db.meteor_accounts_loginServiceConfiguration.insert({
"service" : "facebook",
"appId" : "137758583064594",
"secret" : "3915c1077d25e56fc6444498c6a7984d",
"_id" : "gjpDD9vwGw2tF45ww"
});
db.meteor_accounts_loginServiceConfiguration.insert({
"service" : "twitter",
"consumerKey" : "4HF4e0BhNRR7WwC9WqhRBLPRK",
"secret" : "VSzSnLU2W0dT64a9XGVqKhYo90yAu9pQIJ6McTtIRyRzVopHvT",
"_id" : "FCXK6RmNhKyhjSBQk"
});

View File

@@ -1,61 +0,0 @@
#!/bin/bash
set -e
if [ -z $1 ]; then
echo "This script is to be used in advance of running automated QA on Rainforest"
echo
echo "Usage: ./deploy-example.sh RELEASE"
exit 1
fi
RELEASE=$1
cd `dirname "$0"`/../..
METEOR_ROOT=`pwd`
LOG="$METEOR_ROOT/rainforestqa-deploy.log"
rm $LOG &> /dev/null || true
# Store the original contents in ~/.meteorsession, which contain the
# credentials for the currently logged-in user. Restore that file if
# this script exits.
METEORSESSION_RESTORE="$METEOR_ROOT/.meteorsession-restore"
cp ~/.meteorsession "$METEORSESSION_RESTORE"
function cleanup {
echo "Logs can be found at $METEOR_ROOT/rainforestqa-deploy.log"
cp "$METEORSESSION_RESTORE" ~/.meteorsession
rm "$METEORSESSION_RESTORE"
rm -rf rainforestqa-tmp
}
trap cleanup EXIT
# Now, login as rainforestqa. This way, anyone can access apps
# deployed by this script.
(echo rainforestqa; echo rainforestqa;) | meteor login
PREFIX=rainforest-test
EXAMPLES=`meteor create --list --release $RELEASE | grep '^ ' | cut -c 3-`
# This is where we'll create the example app to be deployed
rm -rf rainforestqa-tmp || true
mkdir rainforestqa-tmp
cd rainforestqa-tmp
# Deploy all example apps
for EXAMPLE in $EXAMPLES ; do
SITE=$PREFIX-$EXAMPLE.meteor.com
# `|| true` so that the script doesn't fail if the the app doesn't exist
meteor deploy -D $SITE >> $LOG 2>&1 || true
meteor create --example $EXAMPLE --release $RELEASE $EXAMPLE >> $LOG 2>&1
cd $EXAMPLE
echo -n "* Deploying $EXAMPLE to $SITE... "
meteor deploy $SITE >> $LOG 2>&1
echo DONE
cd ..
done
# Configure OAuth on parties
cd .. # meteor root
echo -n "* Configuring OAuth for $PREFIX-parties.meteor.com... "
meteor --release $RELEASE mongo $PREFIX-parties.meteor.com >> $LOG 2>&1 < scripts/admin/configure_parties.js
echo DONE

View File

@@ -138,6 +138,17 @@
{
"release": "0.8.1.3"
},
{
"release": "0.8.2",
"packageNotices": {
"accounts-password": [
"Transition to bcrypt for password storage on the server.",
"You do not need to make any changes to your app, but you will",
"not be able to downgrade production apps after you update them",
"to 0.8.2."
]
}
},
{
"release": "NEXT"
}

View File

@@ -74,9 +74,9 @@ cd build
git clone https://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/boot.js, and the text in
# the top of tools/main.js and tools/server/boot.js, and the text in
# docs/client/concepts.html and the README in tools/bundler.js.
git checkout v0.10.28
git checkout v0.10.29
./configure --prefix="$DIR"
make -j4

View File

@@ -1141,25 +1141,31 @@ _.extend(JsImage.prototype, {
if (! item.targetPath)
throw new Error("No targetPath?");
var loadPath = builder.writeToGeneratedFilename(
item.targetPath,
{ data: new Buffer(item.source, 'utf8') });
var loadItem = {
path: loadPath,
node_modules: item.nodeModulesDirectory ?
item.nodeModulesDirectory.preferredBundlePath : undefined
};
if (item.sourceMap) {
// Reference the source map in the source. Looked up later by
// node-inspector.
var sourceMapBaseName = item.targetPath + ".map";
// Write the source map.
// XXX this code is very similar to saveAsUnipackage.
loadItem.sourceMap = builder.writeToGeneratedFilename(
item.targetPath + '.map',
sourceMapBaseName,
{ data: new Buffer(item.sourceMap, 'utf8') }
);
var sourceMapFileName = path.basename(loadItem.sourceMap);
item.source += "\n//# sourceMappingURL=" + sourceMapFileName + "\n";
loadItem.sourceMapRoot = item.sourceMapRoot;
}
loadItem.path = builder.writeToGeneratedFilename(
item.targetPath,
{ data: new Buffer(item.source, 'utf8') });
if (!_.isEmpty(item.assets)) {
// For package code, static assets go inside a directory inside
// assets/packages specific to this package. Application assets (e.g. those
@@ -1536,7 +1542,7 @@ 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.10.28 or newer, plus the 'fibers' module. To run the application:\n" +
"Node.js 0.10.29 or newer, plus the 'fibers' module. To run the application:\n" +
"\n" +
" $ rm -r programs/server/node_modules/fibers\n" +
" $ npm install fibers@1.0.1\n" +

View File

@@ -280,6 +280,21 @@ var printUnauthorizedMessage = function () {
// stripping 'http://' or a trailing '/' if present) and return it. If
// not, print an error message to stderr and return null.
var canonicalizeSite = function (site) {
// There are actually two different bugs here. One is that the meteor deploy
// server does not support apps whose total site length is greater than 63
// (because of how it generates Mongo database names); that can be fixed on
// the server. After that, this check will be too strong, but we still will
// want to check that each *component* of the hostname is at most 63
// characters (url.parse will do something very strange if a component is
// larger than 63, which is the maximum legal length).
if (site.length > 63) {
process.stdout.write(
"The maximum hostname length currently supported is 63 characters.\n" +
site + " is too long.\n" +
"Please try again with a shorter URL for your site.\n");
return false;
}
var url = site;
if (!url.match(':\/\/'))
url = 'http://' + url;

View File

@@ -330,7 +330,7 @@ Fiber(function () {
// Check required Node version.
// This code is duplicated in tools/server/boot.js.
var MIN_NODE_VERSION = 'v0.10.28';
var MIN_NODE_VERSION = 'v0.10.29';
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
process.stderr.write(
'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n');

View File

@@ -6,7 +6,7 @@ var _ = require('underscore');
var sourcemap_support = require('source-map-support');
// This code is duplicated in tools/main.js.
var MIN_NODE_VERSION = 'v0.10.28';
var MIN_NODE_VERSION = 'v0.10.29';
if (require('semver').lt(process.version, MIN_NODE_VERSION)) {
process.stderr.write(
@@ -23,7 +23,7 @@ var configJson =
// Set up environment
__meteor_bootstrap__ = {
startup_hooks: [],
startupHooks: [],
serverDir: serverDir,
configJson: configJson };
__meteor_runtime_config__ = { meteorRelease: configJson.meteorRelease };
@@ -57,7 +57,7 @@ _.each(serverJson.load, function (fileInfo) {
parsedSourceMap.sourceRoot = path.join(
fileInfo.sourceMapRoot, parsedSourceMap.sourceRoot || '');
}
parsedSourceMaps[fileInfo.path] = parsedSourceMap;
parsedSourceMaps[path.resolve(__dirname, fileInfo.path)] = parsedSourceMap;
}
});
@@ -151,12 +151,24 @@ Fiber(function () {
// \n is necessary in case final line is a //-comment
var wrapped = "(function(Npm, Assets){" + code + "\n})";
var func = require('vm').runInThisContext(wrapped, fileInfo.path, true);
// It is safer to use the absolute path when source map is present as
// different tooling, such as node-inspector, can get confused on relative
// urls.
var absoluteFilePath = path.resolve(__dirname, fileInfo.path);
var scriptPath =
parsedSourceMaps[absoluteFilePath] ? absoluteFilePath : fileInfo.path;
var func = require('vm').runInThisContext(wrapped, scriptPath, true);
func.call(global, Npm, Assets); // Coffeescript
});
// run the user startup hooks.
_.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); });
// run the user startup hooks. other calls to startup() during this can still
// add hooks to the end.
while (__meteor_bootstrap__.startupHooks.length) {
var hook = __meteor_bootstrap__.startupHooks.shift();
hook();
}
// Setting this to null tells Meteor.startup to call hooks immediately.
__meteor_bootstrap__.startupHooks = null;
// find and run main()
// XXX hack. we should know the package that contains main.

View File

@@ -19,6 +19,7 @@ selftest.define("login", ['net'], function () {
// even if you are already logged in.
for (var i = 0; i < 2; i++) {
run = s.run("login");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
@@ -31,6 +32,7 @@ selftest.define("login", ['net'], function () {
// Leaving username blank, or getting the password wrong, doesn't
// reprompt. It also doesn't log you out.
run = s.run("login");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("\n");
run.matchErr("Password:");
@@ -40,6 +42,7 @@ selftest.define("login", ['net'], function () {
run.expectExit(1);
run = s.run("login");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
@@ -49,6 +52,7 @@ selftest.define("login", ['net'], function () {
run.expectExit(1);
run = s.run('login');
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
@@ -80,6 +84,7 @@ selftest.define("login", ['net'], function () {
// Test login failure
run = s.run("login");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");
@@ -91,6 +96,7 @@ selftest.define("login", ['net'], function () {
// Logging in with a capitalized username should work (usernames are
// case-insensitive).
run = s.run("login");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("TeSt\n");
run.matchErr("Password:");
@@ -107,6 +113,7 @@ selftest.define("login", ['net'], function () {
// Logging in with a capitalized password should NOT work (can't be
// too safe...)
run = s.run("login");
run.waitSecs(commandTimeoutSecs);
run.matchErr("Username:");
run.write("test\n");
run.matchErr("Password:");

View File

@@ -180,7 +180,7 @@ selftest.define("run --once", function () {
s.cd("onceapp");
s.set("RUN_ONCE_OUTCOME", "mongo");
run = s.run("--once");
run.waitSecs(15);
run.waitSecs(30);
run.expectExit(86);
});
@@ -199,7 +199,7 @@ selftest.define("run errors", function () {
var run = s.run("-p", proxyPort);
_.times(3, function () {
run.waitSecs(3);
run.waitSecs(30);
run.match("Unexpected mongo exit code 48. Restarting.");
});
run.waitSecs(3);

View File

@@ -66,7 +66,7 @@ var load = function (options) {
// will get refactored before too long. Note that
// __meteor_bootstrap__.require is no longer provided.
var env = {
__meteor_bootstrap__: { startup_hooks: [] },
__meteor_bootstrap__: { startupHooks: [] },
__meteor_runtime_config__: { meteorRelease: options.release }
};
@@ -83,7 +83,12 @@ var load = function (options) {
ret = image.load(env);
// Run any user startup hooks.
_.each(env.__meteor_bootstrap__.startup_hooks, function (x) { x(); });
while (env.__meteor_bootstrap__.startupHooks.length) {
var hook = env.__meteor_bootstrap__.startupHooks.shift();
hook();
}
// Setting this to null tells Meteor.startup to call hooks immediately.
env.__meteor_bootstrap__.startupHooks = null;
});
if (messages.hasMessages()) {