diff --git a/.mailmap b/.mailmap index 42f74158ab..efd15a1605 100644 --- a/.mailmap +++ b/.mailmap @@ -12,6 +12,7 @@ GITHUB: aldeed GITHUB: AlexeyMK GITHUB: apendua GITHUB: arbesfeld +GITHUB: Cangit GITHUB: DenisGorbachev GITHUB: EOT GITHUB: FooBarWidget @@ -20,15 +21,21 @@ GITHUB: OyoKooN GITHUB: RobertLowe GITHUB: ansman GITHUB: awwx +GITHUB: babenzele GITHUB: cmather GITHUB: codeinthehole GITHUB: dandv GITHUB: davegonzalez +GITHUB: ducdigital GITHUB: emgee3 +GITHUB: felixrabe +GITHUB: FredericoC GITHUB: icellan GITHUB: jacott GITHUB: jfhamlin +GITHUB: jbruni GITHUB: justinsb +GITHUB: kentonv GITHUB: marcandre GITHUB: mart-jansink GITHUB: meawoppl @@ -47,7 +54,10 @@ GITHUB: rgould GITHUB: ryw GITHUB: rzymek GITHUB: sdarnell +GITHUB: subhog +GITHUB: tbjers GITHUB: timhaines +GITHUB: tmeasday GITHUB: yeputons GITHUB: zol @@ -65,4 +75,3 @@ METEOR: sixolet METEOR: Slava METEOR: stubailo METEOR: ekatek - diff --git a/History.md b/History.md index 50ded1a9e7..e7240bf432 100644 --- a/History.md +++ b/History.md @@ -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: }`) 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 diff --git a/docs/.meteor/release b/docs/.meteor/release index db5f2c74b7..100435be13 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/docs/client/api.html b/docs/client/api.html index d2208a75f3..adc97f191e 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -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> diff --git a/docs/client/api.js b/docs/client/api.js index 11f0fbc067..e1d5d24746 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -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." } ] }; diff --git a/docs/client/commandline.html b/docs/client/commandline.html index 27e28ccef4..bc36e6fef5 100644 --- a/docs/client/commandline.html +++ b/docs/client/commandline.html @@ -2,7 +2,7 @@ <div> {{#markdown}} -{{#api_section "commandline"}}Command line{{/api_section}} +<h1 id="commandline">Command line</h1> <!-- XXX some intro text? --> diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 0ed41a18b3..e73cd7a8f8 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -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 diff --git a/docs/lib/release-override.js b/docs/lib/release-override.js index 2b39cfa5e4..d0fc420710 100644 --- a/docs/lib/release-override.js +++ b/docs/lib/release-override.js @@ -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; } diff --git a/examples/clock/.meteor/release b/examples/clock/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/clock/.meteor/release +++ b/examples/clock/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index db5f2c74b7..100435be13 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.8.1.3 +0.8.2 diff --git a/meteor b/meteor index 0e8875133c..93ddb8cc13 100755 --- a/meteor +++ b/meteor @@ -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. diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 4bb346a578..95c78c7cf3 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -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."); diff --git a/packages/accounts-meteor-developer/meteor-developer.js b/packages/accounts-meteor-developer/meteor-developer.js index 55e131b798..79ac4e4c2c 100644 --- a/packages/accounts-meteor-developer/meteor-developer.js +++ b/packages/accounts-meteor-developer/meteor-developer.js @@ -10,7 +10,7 @@ if (Meteor.isClient) { var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback); - MeteorDeveloperAccounts.requestCredential(credentialRequestCompleteCallback); + MeteorDeveloperAccounts.requestCredential(options, credentialRequestCompleteCallback); }; } else { Accounts.addAutopublishFields({ diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index f53f8c2d10..95f5804885 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -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']); diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js index 96be0b1092..bc04cc8754 100644 --- a/packages/accounts-password/password_client.js +++ b/packages/accounts-password/password_client.js @@ -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}); }; diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index a758da4a1c..280c9e4802 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -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) diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index bd1ce9a6d1..e1e51d4aac 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -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); diff --git a/packages/accounts-password/password_tests_setup.js b/packages/accounts-password/password_tests_setup.js index e9d110c936..fa4432f097 100644 --- a/packages/accounts-password/password_tests_setup.js +++ b/packages/accounts-password/password_tests_setup.js @@ -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"); + } +}); diff --git a/packages/blaze/attrs.js b/packages/blaze/attrs.js index f09a7e0623..d3c5e0f657 100644 --- a/packages/blaze/attrs.js +++ b/packages/blaze/attrs.js @@ -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); diff --git a/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json b/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json index f2a90d9a9f..94ab2fc445 100644 --- a/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json +++ b/packages/less/.npm/plugin/compileLess/npm-shrinkwrap.json @@ -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" diff --git a/packages/less/package.js b/packages/less/package.js index 75e5a1b580..08b6a3efff 100644 --- a/packages/less/package.js +++ b/packages/less/package.js @@ -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'); }); diff --git a/packages/less/plugin/compile-less.js b/packages/less/plugin/compile-less.js index 5b00d99293..7eccb5e42f 100644 --- a/packages/less/plugin/compile-less.js +++ b/packages/less/plugin/compile-less.js @@ -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; diff --git a/packages/livedata/livedata_connection.js b/packages/livedata/livedata_connection.js index b3468cad37..5502857651 100644 --- a/packages/livedata/livedata_connection.js +++ b/packages/livedata/livedata_connection.js @@ -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. diff --git a/packages/meteor-developer/meteor_developer_client.js b/packages/meteor-developer/meteor_developer_client.js index 239afec978..82d7ca7c98 100644 --- a/packages/meteor-developer/meteor_developer_client.js +++ b/packages/meteor-developer/meteor_developer_client.js @@ -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, diff --git a/packages/meteor/helpers_test.js b/packages/meteor/helpers_test.js index ec5afd3859..14d083f085 100644 --- a/packages/meteor/helpers_test.js +++ b/packages/meteor/helpers_test.js @@ -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); +}); diff --git a/packages/meteor/startup_server.js b/packages/meteor/startup_server.js index af0ca8126c..2a08396ac1 100644 --- a/packages/meteor/startup_server.js +++ b/packages/meteor/startup_server.js @@ -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(); + } }; diff --git a/packages/observe-sequence/observe_sequence.js b/packages/observe-sequence/observe_sequence.js index 7af183f084..e4c1b4dea2 100644 --- a/packages/observe-sequence/observe_sequence.js +++ b/packages/observe-sequence/observe_sequence.js @@ -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; diff --git a/packages/observe-sequence/observe_sequence_tests.js b/packages/observe-sequence/observe_sequence_tests.js index 549db4283f..340de1179c 100644 --- a/packages/observe-sequence/observe_sequence_tests.js +++ b/packages/observe-sequence/observe_sequence_tests.js @@ -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) { diff --git a/packages/sha/.gitignore b/packages/sha/.gitignore new file mode 100644 index 0000000000..677a6fc263 --- /dev/null +++ b/packages/sha/.gitignore @@ -0,0 +1 @@ +.build* diff --git a/packages/sha/package.js b/packages/sha/package.js new file mode 100644 index 0000000000..fbf93e6c8d --- /dev/null +++ b/packages/sha/package.js @@ -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']); +}); diff --git a/packages/srp/sha256.js b/packages/sha/sha256.js similarity index 96% rename from packages/srp/sha256.js rename to packages/sha/sha256.js index 4743264b4e..675f87ef1a 100644 --- a/packages/srp/sha256.js +++ b/packages/sha/sha256.js @@ -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 diff --git a/packages/spacebars-tests/template_tests.html b/packages/spacebars-tests/template_tests.html index f832d8f35b..85a0c50dce 100644 --- a/packages/spacebars-tests/template_tests.html +++ b/packages/spacebars-tests/template_tests.html @@ -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> diff --git a/packages/spacebars-tests/template_tests.js b/packages/spacebars-tests/template_tests.js index b21b4678cb..1c8a92f50b 100644 --- a/packages/spacebars-tests/template_tests.js +++ b/packages/spacebars-tests/template_tests.js @@ -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"); + } +); diff --git a/packages/srp/package.js b/packages/srp/package.js index f304b8dccc..674a14437a 100644 --- a/packages/srp/package.js +++ b/packages/srp/package.js @@ -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']); }); diff --git a/packages/srp/srp.js b/packages/srp/srp.js index 099848c569..1fd7344007 100644 --- a/packages/srp/srp.js +++ b/packages/srp/srp.js @@ -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); -}; diff --git a/packages/srp/srp_tests.js b/packages/srp/srp_tests.js index d1ea3edc35..597b4cf4df 100644 --- a/packages/srp/srp_tests.js +++ b/packages/srp/srp_tests.js @@ -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"); }); diff --git a/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json b/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json index f1651f24a7..b04c343ccb 100644 --- a/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json +++ b/packages/stylus/.npm/plugin/compileStylus/npm-shrinkwrap.json @@ -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" } } } diff --git a/packages/stylus/package.js b/packages/stylus/package.js index c3b995389c..371d6528e6 100644 --- a/packages/stylus/package.js +++ b/packages/stylus/package.js @@ -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) { diff --git a/packages/test-helpers/canonicalize_html.js b/packages/test-helpers/canonicalize_html.js index 424fa13dce..5ac2c0e277 100644 --- a/packages/test-helpers/canonicalize_html.js +++ b/packages/test-helpers/canonicalize_html.js @@ -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(' ')+'>'; diff --git a/packages/ui/package.js b/packages/ui/package.js index 3d33314e5b..3631ce9f83 100644 --- a/packages/ui/package.js +++ b/packages/ui/package.js @@ -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'); diff --git a/packages/ui/render_tests.js b/packages/ui/render_tests.js index ee4ba1ae22..6771fb5275 100644 --- a/packages/ui/render_tests.js +++ b/packages/ui/render_tests.js @@ -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) { diff --git a/scripts/admin/banner.txt b/scripts/admin/banner.txt index 748c205ee0..c72d6b8ddb 100644 --- a/scripts/admin/banner.txt +++ b/scripts/admin/banner.txt @@ -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'. diff --git a/scripts/admin/configure_parties.js b/scripts/admin/configure_parties.js deleted file mode 100644 index d60677747d..0000000000 --- a/scripts/admin/configure_parties.js +++ /dev/null @@ -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" -}); diff --git a/scripts/admin/deploy-examples.sh b/scripts/admin/deploy-examples.sh deleted file mode 100755 index 8377680bbe..0000000000 --- a/scripts/admin/deploy-examples.sh +++ /dev/null @@ -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 diff --git a/scripts/admin/notices.json b/scripts/admin/notices.json index ab738757bc..3b002c98b5 100644 --- a/scripts/admin/notices.json +++ b/scripts/admin/notices.json @@ -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" } diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index 3346bb6259..d0738cad4f 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -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 diff --git a/tools/bundler.js b/tools/bundler.js index 2244bf0de6..8c4f9d2b5b 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -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" + diff --git a/tools/deploy.js b/tools/deploy.js index d5061b3b91..0d017fb0c4 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -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; diff --git a/tools/main.js b/tools/main.js index f7cc871a4b..af83746a1c 100644 --- a/tools/main.js +++ b/tools/main.js @@ -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'); diff --git a/tools/server/boot.js b/tools/server/boot.js index 2076c92c2a..ac5d08b92f 100644 --- a/tools/server/boot.js +++ b/tools/server/boot.js @@ -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. diff --git a/tools/tests/login.js b/tools/tests/login.js index 876eeffe7c..0e793bf3f5 100644 --- a/tools/tests/login.js +++ b/tools/tests/login.js @@ -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:"); diff --git a/tools/tests/run.js b/tools/tests/run.js index 80e4544544..cc114a024e 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -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); diff --git a/tools/unipackage.js b/tools/unipackage.js index 53da2f00ad..b4854329f7 100644 --- a/tools/unipackage.js +++ b/tools/unipackage.js @@ -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()) {