From 411c6cf8f4868d1b7e1762a67213b0974a3e8818 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 23 Sep 2013 14:58:21 -0700 Subject: [PATCH 01/35] Update to Node 0.10.18, bumping dev bundle version. Use caronte. --- docs/client/concepts.html | 4 ++-- meteor | 2 +- scripts/generate-dev-bundle.sh | 8 ++++++-- tools/bundler.js | 4 ++-- tools/meteor.js | 2 +- tools/server/boot.js | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 61d47d7c18..f0fc8f13e5 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -824,9 +824,9 @@ To get started, run $ meteor bundle myapp.tgz This command will generate a fully-contained Node.js application in the form of -a tarball. To run this application, you need to provide Node.js 0.8 and a +a tarball. To run this application, you need to provide Node.js 0.10 and a MongoDB server. (The current release of Meteor has been tested with Node -0.8.24.) You can then run the application by invoking node, specifying the HTTP +0.10.18.) You can then run the application by invoking node, specifying the HTTP port for the application to listen on, and the MongoDB endpoint. If you don't already have a MongoDB server, we can recommend our friends at [MongoHQ](http://mongohq.com). diff --git a/meteor b/meteor index 5584e6c2a4..58177c2493 100755 --- a/meteor +++ b/meteor @@ -1,6 +1,6 @@ #!/bin/bash -BUNDLE_VERSION=0.3.15 +BUNDLE_VERSION=0.3.16 # OS Check. Put here because here is where we download the precompiled # bundles that are arch specific. diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index 0091c5a388..ee29664782 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -76,7 +76,7 @@ cd node # When upgrading node versions, also update the values of MIN_NODE_VERSION at # the top of tools/meteor.js and tools/server/server.js, and the text in # docs/client/concepts.html and the README in tools/bundler.js. -git checkout v0.8.24 +git checkout v0.10.18 ./configure --prefix="$DIR" make -j4 @@ -102,7 +102,6 @@ npm install semver@1.1.0 npm install handlebars@1.0.7 npm install request@2.12.0 npm install keypress@0.1.0 -npm install http-proxy@0.10.1 # not 0.10.2, which contains a sketchy websocket change npm install underscore@1.5.1 npm install fstream@0.1.21 npm install tar@0.1.14 @@ -111,6 +110,11 @@ npm install shell-quote@0.0.1 # now at 1.3.3, which adds plenty of options to npm install byline@2.0.3 # v3 requires node 0.10 npm install source-map@0.1.26 +# Using the unreleased "caronte" rewrite of http-proxy (which is even called +# 'caronte', though this may change when this eventually hopefully becomes +# http-proxy 1.0). +npm install https://github.com/nodejitsu/node-http-proxy/tarball/94ec6fa5ce6826ca1e8974f7e99b31541aaad76a + # Using the unreleased 1.1 branch. We can probably switch to a built NPM version # when it gets released. npm install https://github.com/ariya/esprima/tarball/5044b87f94fb802d9609f1426c838874ec2007b3 diff --git a/tools/bundler.js b/tools/bundler.js index 3216ef8c2b..e64be55208 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1442,8 +1442,8 @@ var writeSiteArchive = function (targets, outputPath, options) { builder.write('README', { data: new Buffer( "This is a Meteor application bundle. It has only one dependency:\n" + -"Node.js 0.8 (with the 'fibers' package). The current release of Meteor\n" + -"has been tested with Node 0.8.24. To run the application:\n" + +"Node.js 0.10 (with the 'fibers' package). The current release of Meteor\n" + +"has been tested with Node 0.10.18. To run the application:\n" + "\n" + " $ npm install fibers@1.0.1\n" + " $ export MONGO_URL='mongodb://user:password@host:port/databasename'\n" + diff --git a/tools/meteor.js b/tools/meteor.js index 290729f6b7..f5b1ad2e68 100644 --- a/tools/meteor.js +++ b/tools/meteor.js @@ -24,7 +24,7 @@ Fiber(function () { var Future = require('fibers/future'); // This code is duplicated in app/server/server.js. - var MIN_NODE_VERSION = 'v0.8.24'; + var MIN_NODE_VERSION = 'v0.10.18'; 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 0a2fa308a2..c8931b0730 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/server/server.js. -var MIN_NODE_VERSION = 'v0.8.24'; +var MIN_NODE_VERSION = 'v0.10.18'; if (require('semver').lt(process.version, MIN_NODE_VERSION)) { process.stderr.write( 'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n'); From fa4509f81b34cec8cbc5975491e0961800de7e1a Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 15 Apr 2013 14:59:03 -0700 Subject: [PATCH 02/35] Use npm install --force to get around NPM cache corruption bug. (This caused test_bundler_npm to fail sporadically with Node 0.8.) --- tools/meteor_npm.js | 16 +++++++++++++--- tools/tests/test_bundler_npm.js | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index cf07689d95..d89d3e1005 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -407,9 +407,18 @@ _.extend(exports, { // We don't use npm.commands.install since we couldn't // figure out how to silence all output (specifically the // installed tree which is printed out with `console.log`) + // + // We use --force, because the NPM cache is broken! See + // https://github.com/isaacs/npm/issues/3265 Basically, switching back and + // forth between a tarball fork of version X and the real version X can + // confuse NPM. But the main reason to use tarball URLs is to get a fork of + // the latest version with some fix, so it's easy to trigger this! So + // instead, always use --force. (Even with --force, we still WRITE to the + // cache, so we can corrupt the cache for other invocations of npm... ah + // well.) var result = this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"), - ["install", installArg], + ["install", "--force", installArg], {cwd: dir}); if (! result.success) { @@ -436,10 +445,11 @@ _.extend(exports, { this._ensureConnected(); - // `npm install`, which reads npm-shrinkwrap.json + // `npm install`, which reads npm-shrinkwrap.json. See above for why + // --force. var result = this._execFileSync(path.join(files.get_dev_bundle(), "bin", "npm"), - ["install"], {cwd: dir}); + ["install", "--force"], {cwd: dir}); if (! result.success) { diff --git a/tools/tests/test_bundler_npm.js b/tools/tests/test_bundler_npm.js index 82fe94c1dc..e4e1509bc0 100644 --- a/tools/tests/test_bundler_npm.js +++ b/tools/tests/test_bundler_npm.js @@ -172,12 +172,12 @@ assert.doesNotThrow(function () { // while bundling, verify that we don't call `npm install // name@version unnecessarily` -- calling `npm install` is enough, - // and installing each package separately coul unintentionally bump + // and installing each package separately could unintentionally bump // subdependency versions. (to intentionally bump subdependencies, // just remove all of the .npm directory) var bareExecFileSync = meteorNpm._execFileSync; meteorNpm._execFileSync = function(file, args, opts) { - if (args[0] === 'install' && args[1]) + if (args.length > 2 && args[0] === 'install' && args[1] === '--force') assert.fail("shouldn't be installing specific npm packages: " + args[1]); return bareExecFileSync(file, args, opts); }; From d1cf1bd2e389e0ab8db80de5f9e48331241fb719 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 23 Sep 2013 19:30:32 -0700 Subject: [PATCH 03/35] Make sure all packages rebuild. This isn't quite enough to rebuild NPM packages but it's a start. --- tools/packages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/packages.js b/tools/packages.js index 41832c04fd..677cb40d00 100644 --- a/tools/packages.js +++ b/tools/packages.js @@ -26,7 +26,7 @@ var sourcemap = require('source-map'); // end up as watched dependencies. (At least for now, packages only used in // target creation (eg minifiers and dev-bundle-fetcher) don't require you to // update BUILT_BY, though you will need to quit and rerun "meteor run".) -exports.BUILT_BY = 'meteor/8'; +exports.BUILT_BY = 'meteor/9'; // Like Perl's quotemeta: quotes all regexp metacharacters. See // https://github.com/substack/quotemeta/blob/master/index.js From 5f146a9d72392809488f1b04ba42f35c66a9bc63 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 23 Sep 2013 23:29:34 -0700 Subject: [PATCH 04/35] Update run.js to use caronte API. --- tools/run.js | 99 +++++++++++++++++++++------------------------------- 1 file changed, 39 insertions(+), 60 deletions(-) diff --git a/tools/run.js b/tools/run.js index e2348459e0..292007d943 100644 --- a/tools/run.js +++ b/tools/run.js @@ -108,8 +108,16 @@ var requestQueue = []; var startProxy = function (outerPort, innerPort, callback) { callback = callback || function () {}; - var httpProxy = require('http-proxy'); - var p = httpProxy.createServer(function (req, res, proxy) { + var http = require('http'); + // caronte is the code name for http-proxy 1.0 while it's under + // development. Once it's released, we may need to adjust the APIs slightly. + // (eg, the name of the event on proxy.ev will probably no longer say + // "caronte") + var caronte = require('caronte'); + + var proxy = caronte.createProxyServer({}); + + var server = http.createServer(function (req, res) { if (Status.crashing) { // sad face. send error logs. // XXX formatting! text/plain is bad @@ -126,43 +134,35 @@ var startProxy = function (outerPort, innerPort, callback) { }); res.end(); - } else if (Status.listening) { - // server is listening. things are hunky dory! - proxy.proxyRequest(req, res, { - host: '127.0.0.1', port: innerPort - }); - } else { - // Not listening yet. Queue up request. - var buffer = httpProxy.buffer(req); - requestQueue.push(function () { - proxy.proxyRequest(req, res, { - host: '127.0.0.1', port: innerPort, - buffer: buffer - }); - }); + return; } - }); - - // Proxy websocket requests using same buffering logic as for regular HTTP requests - p.on('upgrade', function(req, socket, head) { + var proxyIt = function () { + proxy.web(req, res, {target: 'http://127.0.0.1:' + innerPort}); + }; if (Status.listening) { // server is listening. things are hunky dory! - p.proxy.proxyWebSocketRequest(req, socket, head, { - host: '127.0.0.1', port: innerPort - }); + proxyIt(); } else { - // Not listening yet. Queue up request. - var buffer = httpProxy.buffer(req); - requestQueue.push(function () { - p.proxy.proxyWebSocketRequest(req, socket, head, { - host: '127.0.0.1', port: innerPort, - buffer: buffer - }); - }); + requestQueue.push(proxyIt); } }); - p.on('error', function (err) { + // Proxy websocket requests using same buffering logic as for regular HTTP + // requests + server.on('upgrade', function(req, socket, head) { + var proxyIt = function () { + proxy.ws(req, socket, head, { target: 'http://127.0.0.1:' + innerPort}); + }; + if (Status.listening) { + // server is listening. things are hunky dory! + proxyIt(); + } else { + // Not listening yet. Queue up request. + requestQueue.push(proxyIt); + } + }); + + server.on('error', function (err) { if (err.code == 'EADDRINUSE') { process.stderr.write("Can't listen on port " + outerPort + ". Perhaps another Meteor is running?\n"); @@ -177,17 +177,19 @@ var startProxy = function (outerPort, innerPort, callback) { process.exit(1); }); - // don't spin forever if the app doesn't respond. instead return an - // error immediately. This shouldn't happen much since we try to not - // send requests if the app is down. - p.proxy.on('proxyError', function (err, req, res) { + // don't crash if the app doesn't respond. instead return an error + // immediately. This shouldn't happen much since we try to not send requests + // if the app is down. + // XXX should we also handle caronte:outgoing:ws:error, for a failed + // websocket? + proxy.ee.on('caronte:outgoing:web:error', function (err, req, res) { res.writeHead(503, { 'Content-Type': 'text/plain' }); res.end('Unexpected error.'); }); - p.listen(outerPort, callback); + server.listen(outerPort, callback); }; var saveLog = function (msg) { @@ -402,29 +404,6 @@ exports.run = function (context, options) { ("mongodb://127.0.0.1:" + mongoPort + "/meteor"); var firstRun = true; - // node-http-proxy doesn't properly handle errors if it has a problem writing - // to the proxy target. While we try to not proxy requests when we don't think - // the target is listening, there are race conditions here, and in any case - // those attempts don't take effect for pre-existing websocket connections. - // Error handling in node-http-proxy is really convoluted and will change with - // their ongoing Node 0.10.x compatible rewrite, so rather than trying to - // debug and send pull request now, we'll wait for them to finish their - // rewrite. In the meantime, ignore two common exceptions that we sometimes - // see instead of crashing. - // - // See https://github.com/meteor/meteor/issues/513 - // - // That bug is about "meteor deploy"s use of http-proxy, but it also affects - // our use here; see - // https://groups.google.com/d/msg/meteor-core/JgbnfKEa5lA/FJHZtJftfSsJ - // - // XXX remove this once we've upgraded and fixed http-proxy - process.on('uncaughtException', function (e) { - if (e && (e.errno === 'EPIPE' || e.message === "This socket is closed.")) - return; - throw e; - }); - var serverHandle; var watcher; From dd7172ab7fc7141b68b766cf5c600efb0dde2739 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 24 Sep 2013 00:35:15 -0700 Subject: [PATCH 05/35] Use an http agent (this was default in old http-proxy but not in caronte). --- tools/run.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/run.js b/tools/run.js index 292007d943..311bcbbb40 100644 --- a/tools/run.js +++ b/tools/run.js @@ -115,7 +115,11 @@ var startProxy = function (outerPort, innerPort, callback) { // "caronte") var caronte = require('caronte'); - var proxy = caronte.createProxyServer({}); + var proxy = caronte.createProxyServer({ + // agent is required to handle keep-alive, and caronte is a little buggy + // without it: https://github.com/nodejitsu/node-http-proxy/pull/488 + agent: new http.Agent({maxSockets: 100}) + }); var server = http.createServer(function (req, res) { if (Status.crashing) { From ca1fb6788abbc03cf5f3f5265c71a716c5a248c0 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 24 Sep 2013 17:12:39 -0700 Subject: [PATCH 06/35] Track the node version used when installing NPM modules. This way, when the dev bundle version changes the Node version they will get reinstalled. (You still need to bump BUILT_BY to get the modules into unipackages too.) --- tools/meteor_npm.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index d89d3e1005..780958fd28 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -186,6 +186,26 @@ _.extend(exports, { throw new Error( "Corrupted .npm directory -- can't find npm-shrinkwrap.json in " + packageNpmDir); + // We need to rebuild all node modules when the Node version changes, in + // case there are some binary ones. Technically this is racey, but it + // shouldn't fail very often. + if (fs.existsSync(path.join(packageNpmDir, 'node_modules'))) { + var oldNodeVersion; + try { + oldNodeVersion = fs.readFileSync( + path.join(packageNpmDir, 'node_modules', '.node_version')); + } catch (e) { + if (e.code !== 'ENOENT') + throw e; + // Use the Node version from the last release where we didn't drop this + // file. + oldNodeVersion = 'v0.8.24'; + } + + if (oldNodeVersion !== process.version) + files.rm_recursive(path.join(packageNpmDir, 'node_modules')); + } + var installedDependencies = self._installedDependencies(packageNpmDir); // If we already have the right things installed, life is good. @@ -276,6 +296,7 @@ _.extend(exports, { fs.unlinkSync(path.join(newPackageNpmDir, 'package.json')); self._createReadme(newPackageNpmDir); + self._createNodeVersion(newPackageNpmDir); files.renameDirAlmostAtomically(newPackageNpmDir, packageNpmDir); }, @@ -292,6 +313,12 @@ _.extend(exports, { ); }, + _createNodeVersion: function(newPackageNpmDir) { + fs.writeFileSync( + path.join(newPackageNpmDir, 'node_modules', '.node_version'), + process.version); + }, + // Returns object with keys 'stdout', 'stderr', and 'success' (true // for clean exit with exit code 0, else false) _execFileSync: function(file, args, opts) { From 6c1f92e89156e529b2da3555d65eb9094ff4e9d9 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 24 Sep 2013 23:31:47 -0700 Subject: [PATCH 07/35] Might as well use the version that just got released (0.10.19). --- docs/client/concepts.html | 2 +- scripts/generate-dev-bundle.sh | 4 ++-- tools/bundler.js | 2 +- tools/meteor.js | 2 +- tools/server/boot.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/client/concepts.html b/docs/client/concepts.html index f0fc8f13e5..67e13a9852 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -826,7 +826,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.18.) You can then run the application by invoking node, specifying the HTTP +0.10.19.) You can then run the application by invoking node, specifying the HTTP port for the application to listen on, and the MongoDB endpoint. If you don't already have a MongoDB server, we can recommend our friends at [MongoHQ](http://mongohq.com). diff --git a/scripts/generate-dev-bundle.sh b/scripts/generate-dev-bundle.sh index ee29664782..86bea06977 100755 --- a/scripts/generate-dev-bundle.sh +++ b/scripts/generate-dev-bundle.sh @@ -74,9 +74,9 @@ cd build git clone git://github.com/joyent/node.git cd node # When upgrading node versions, also update the values of MIN_NODE_VERSION at -# the top of tools/meteor.js and tools/server/server.js, and the text in +# the top of tools/meteor.js and tools/server/boot.js, and the text in # docs/client/concepts.html and the README in tools/bundler.js. -git checkout v0.10.18 +git checkout v0.10.19 ./configure --prefix="$DIR" make -j4 diff --git a/tools/bundler.js b/tools/bundler.js index e64be55208..22527a7cca 100644 --- a/tools/bundler.js +++ b/tools/bundler.js @@ -1443,7 +1443,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 (with the 'fibers' package). The current release of Meteor\n" + -"has been tested with Node 0.10.18. To run the application:\n" + +"has been tested with Node 0.10.19. To run the application:\n" + "\n" + " $ npm install fibers@1.0.1\n" + " $ export MONGO_URL='mongodb://user:password@host:port/databasename'\n" + diff --git a/tools/meteor.js b/tools/meteor.js index f5b1ad2e68..db2d770aa7 100644 --- a/tools/meteor.js +++ b/tools/meteor.js @@ -24,7 +24,7 @@ Fiber(function () { var Future = require('fibers/future'); // This code is duplicated in app/server/server.js. - var MIN_NODE_VERSION = 'v0.10.18'; + var MIN_NODE_VERSION = 'v0.10.19'; if (require('semver').lt(process.version, MIN_NODE_VERSION)) { process.stderr.write( 'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n'); diff --git a/tools/server/boot.js b/tools/server/boot.js index c8931b0730..091aee0ff1 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/server/server.js. -var MIN_NODE_VERSION = 'v0.10.18'; +var MIN_NODE_VERSION = 'v0.10.19'; if (require('semver').lt(process.version, MIN_NODE_VERSION)) { process.stderr.write( 'Meteor requires Node ' + MIN_NODE_VERSION + ' or later.\n'); From 25aae35422c668e14bb02faf069cf52f3b0f500e Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 25 Sep 2013 01:57:50 -0700 Subject: [PATCH 08/35] Fix .node_version check. run-tools-tests passes. --- tools/meteor_npm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index 780958fd28..67fb840d33 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -193,7 +193,7 @@ _.extend(exports, { var oldNodeVersion; try { oldNodeVersion = fs.readFileSync( - path.join(packageNpmDir, 'node_modules', '.node_version')); + path.join(packageNpmDir, 'node_modules', '.node_version'), 'utf8'); } catch (e) { if (e.code !== 'ENOENT') throw e; From ad705d5ee08c1a72f4b23b90b1dcf638504a0229 Mon Sep 17 00:00:00 2001 From: Andrew Mao Date: Tue, 24 Sep 2013 22:15:25 -0400 Subject: [PATCH 09/35] Update dependencies for madewith package; fixes #1448 --- packages/madewith/package.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/madewith/package.js b/packages/madewith/package.js index b67b92bf80..79cb20411a 100644 --- a/packages/madewith/package.js +++ b/packages/madewith/package.js @@ -3,7 +3,7 @@ Package.describe({ }); Package.on_use(function (api) { - api.use(['livedata', 'underscore', 'spark', 'templating'], 'client'); + api.use(['livedata', 'mongo-livedata', 'underscore', 'spark', 'templating'], 'client'); api.add_files([ 'madewith.css', From a3d539ed0bab7b8fa0f72cb7daf6467ba0aec17c Mon Sep 17 00:00:00 2001 From: Matt DeBergalis Date: Wed, 25 Sep 2013 11:39:24 -0700 Subject: [PATCH 10/35] Disallow words added after game clock expires. Fixes #541. --- examples/wordplay/model.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/wordplay/model.js b/examples/wordplay/model.js index 3851c2639f..5c609e4376 100644 --- a/examples/wordplay/model.js +++ b/examples/wordplay/model.js @@ -102,9 +102,11 @@ Meteor.methods({ var word = Words.findOne(word_id); var game = Games.findOne(word.game_id); - // client and server can both check: must be at least three chars - // long, not already used, and possible to make on the board. - if (word.length < 3 + // client and server can both check that the game has time remaining, and + // that the word is at least three chars, isn't already used, and is + // possible to make on the board. + if (game.clock === 0 + || word.length < 3 || Words.find({game_id: word.game_id, word: word.word}).count() > 1 || paths_for_word(game.board, word.word).length === 0) { Words.update(word._id, {$set: {score: 0, state: 'bad'}}); From 569e92201a939ff838255a7fb09c16d0dabae861 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Wed, 25 Sep 2013 11:57:49 -0700 Subject: [PATCH 11/35] Add XXX to reload package informed by #657 --- packages/reload/reload.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/reload/reload.js b/packages/reload/reload.js index 2c2f412e7e..6aae7f191b 100644 --- a/packages/reload/reload.js +++ b/packages/reload/reload.js @@ -26,6 +26,11 @@ * the client's session to render properly. */ +// XXX when making this API public, also expose a flag for the app +// developer to know whether a hot code push is happening. This is +// useful for apps using `window.onbeforeunload`. See +// https://github.com/meteor/meteor/pull/657 + var KEY_NAME = 'Meteor_Reload'; // after how long should we consider this no longer an automatic // reload, but a fresh restart. This only happens if a reload is From 5889adea901ab017748ea6a0bfdb7437e0db0a7a Mon Sep 17 00:00:00 2001 From: Matt DeBergalis Date: Wed, 25 Sep 2013 12:13:03 -0700 Subject: [PATCH 12/35] Various wordplay fixes: * Don't add blank lines to the dictionary. * Fix broken word length check in `score_word`. * Prevent event handler from submitting blank words. --- examples/wordplay/client/wordplay.js | 5 ++--- examples/wordplay/model.js | 7 ++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/wordplay/client/wordplay.js b/examples/wordplay/client/wordplay.js index f49212923b..acff55aaeb 100644 --- a/examples/wordplay/client/wordplay.js +++ b/examples/wordplay/client/wordplay.js @@ -132,9 +132,8 @@ Template.scratchpad.events({ 'click button, keyup input': function (evt) { var textbox = $('#scratchpad input'); // if we clicked the button or hit enter - if (evt.type === "click" || - (evt.type === "keyup" && evt.which === 13)) { - + if ((evt.type === "click" || (evt.type === "keyup" && evt.which === 13)) + && textbox.val()) { var word_id = Words.insert({player_id: Session.get('player_id'), game_id: game() && game()._id, word: textbox.val().toUpperCase(), diff --git a/examples/wordplay/model.js b/examples/wordplay/model.js index 5c609e4376..416c4db122 100644 --- a/examples/wordplay/model.js +++ b/examples/wordplay/model.js @@ -106,7 +106,8 @@ Meteor.methods({ // that the word is at least three chars, isn't already used, and is // possible to make on the board. if (game.clock === 0 - || word.length < 3 + || !word.word + || word.word.length < 3 || Words.find({game_id: word.game_id, word: word.word}).count() > 1 || paths_for_word(game.board, word.word).length === 0) { Words.update(word._id, {$set: {score: 0, state: 'bad'}}); @@ -129,8 +130,8 @@ Meteor.methods({ if (Meteor.isServer) { DICTIONARY = {}; _.each(Assets.getText("enable2k.txt").split("\n"), function (line) { - // Skip comment lines - if (line.indexOf("//") !== 0) { + // Skip blanks and comment lines + if (line && line.indexOf("//") !== 0) { DICTIONARY[line] = true; } }); From 2413a8d3ed5ffb10e7deb20c3e6292b96e4a2a0e Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 24 Sep 2013 11:35:12 -0700 Subject: [PATCH 13/35] Use cryptographic PRNGs when available. This means node's crypto.randomBytes on the server, and window.crypto.getRandomValues on the client. If node's crypto.randomBytes throws an exception, we fall back to crypto.pseudoRandomBytes. If window.crypto.getRandomValues isn't supported by the browser, we fall back to the alea generator that we had been using previously. --- docs/client/packages/random.html | 12 +-- packages/random/random.js | 144 ++++++++++++++++--------- packages/random/random_tests.js | 14 +++ packages/spark/spark_tests.js | 2 +- packages/test-helpers/seeded_random.js | 2 +- 5 files changed, 113 insertions(+), 61 deletions(-) diff --git a/docs/client/packages/random.html b/docs/client/packages/random.html index 8f0490613d..72c7ce9f51 100644 --- a/docs/client/packages/random.html +++ b/docs/client/packages/random.html @@ -3,8 +3,11 @@ ## `random` The `random` package provides several functions for generating random -numbers. It uses a Meteor-provided random number generator that does not depend -on the browser's facilities. +numbers. It uses a cryptographically strong pseudorandom number generator when +possible, but falls back to a weaker random number generator when +cryptographically strong randomness is not available (on older browsers or on +servers that don't have enough entropy to seed the cryptographically strong +generator).
{{#dtdd "Random.id()"}} @@ -25,10 +28,5 @@ Returns a random string of `n` hexadecimal digits. {{/dtdd}}
-{{#note}} -In the current implementation, random values do not come from a -cryptographically strong pseudorandom number generator. Future releases will -improve this, particularly on the server. -{{/note}} {{/better_markdown}} diff --git a/packages/random/random.js b/packages/random/random.js index 5faddb9d4c..59e94683e8 100644 --- a/packages/random/random.js +++ b/packages/random/random.js @@ -1,3 +1,15 @@ +// We use cryptographically strong PRNGs (crypto.getRandomBytes() on the server, +// window.crypto.getRandomValues() in the browser) when available. If these +// PRNGs fail, we fall back to the Alea PRNG, which is not cryptographically +// strong, and we seed it with various sources such as the date, Math.random, +// and window size on the client. When using crypto.getRandomValues(), our +// primitive is hexString(), from which we construct fraction(). When using +// window.crypto.getRandomValues() or alea, the primitive is fraction and we use +// that to construct hex string. + +if (Meteor.isServer) + var nodeCrypto = Npm.require('crypto'); + // see http://baagoe.org/en/wiki/Better_random_numbers_for_javascript // for a full discussion and Alea implementation. var Alea = function () { @@ -75,52 +87,79 @@ var Alea = function () { var UNMISTAKABLE_CHARS = "23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz"; -var create = function (/* arguments */) { - - var random = Alea.apply(null, arguments); - - var self = {}; - - var bind = function (fn) { - return _.bind(fn, self); - }; - - return _.extend(self, { - _Alea: Alea, - - create: create, - - fraction: random, - - choice: bind(function (arrayOrString) { - var index = Math.floor(this.fraction() * arrayOrString.length); - if (typeof arrayOrString === "string") - return arrayOrString.substr(index, 1); - else - return arrayOrString[index]; - }), - - id: bind(function() { - var digits = []; - // Length of 17 preserves around 96 bits of entropy, which is the - // amount of state in our PRNG - for (var i = 0; i < 17; i++) { - digits[i] = this.choice(UNMISTAKABLE_CHARS); - } - return digits.join(""); - }), - - hexString: bind(function (digits) { - var hexDigits = []; - for (var i = 0; i < digits; ++i) { - hexDigits.push(this.choice("0123456789abcdef")); - } - return hexDigits.join(''); - }) - }); +// If seeds are provided, then the alea PRNG will be used, since cryptographic +// PRNGs (Node crypto and window.crypto.getRandomValues) don't allow us to +// specify seeds. The caller is responsible for making sure to provide a seed +// for alea if a csprng is not available. +var RandomGenerator = function (seedArray) { + var self = this; + if (seedArray !== undefined) + self.alea = Alea.apply(null, seedArray); + self._Alea = Alea; }; -// instantiate RNG. Heuristically collect entropy from various sources +RandomGenerator.prototype.fraction = function () { + var self = this; + if (self.alea) { + return self.alea(); + } else if (nodeCrypto) { + var numerator = parseInt(self.hexString(8), 16); + return numerator * 2.3283064365386963e-10; // 2^-32 + } else if (typeof window !== "undefined" && window.crypto && + window.crypto.getRandomValues) { + var array = new Uint32Array(1); + window.crypto.getRandomValues(array); + return array[0] * 2.3283064365386963e-10; // 2^-32 + } +}; + +RandomGenerator.prototype.hexString = function (digits) { + var self = this; + if (nodeCrypto && ! self.alea) { + var numBytes = Math.ceil(digits / 2); + var bytes; + // Try to get cryptographically strong randomness. Fall back to + // non-cryptographically strong if not available. + try { + bytes = nodeCrypto.randomBytes(numBytes); + } catch (e) { + // XXX should re-throw any error except insufficient entropy + bytes = nodeCrypto.pseudoRandomBytes(numBytes); + } + var result = bytes.toString("hex"); + // If the number of digits is odd, we'll have generated an extra 4 bits + // of randomness, so we need to trim the last digit. + return result.substring(0, digits); + } else { + var hexDigits = []; + for (var i = 0; i < digits; ++i) { + hexDigits.push(self.choice("0123456789abcdef")); + } + return hexDigits.join(''); + } +}; + +RandomGenerator.prototype.id = function () { + var digits = []; + var self = this; + // Length of 17 preserves around 96 bits of entropy, which is the + // amount of state in the Alea PRNG. + for (var i = 0; i < 17; i++) { + digits[i] = self.choice(UNMISTAKABLE_CHARS); + } + return digits.join(""); +}; + +RandomGenerator.prototype.choice = function (arrayOrString) { + var index = Math.floor(this.fraction() * arrayOrString.length); + if (typeof arrayOrString === "string") + return arrayOrString.substr(index, 1); + else + return arrayOrString[index]; +}; + +// instantiate RNG. Heuristically collect entropy from various sources when a +// cryptographic PRNG isn't available. // client sources var height = (typeof window !== 'undefined' && window.innerHeight) || @@ -143,12 +182,13 @@ var width = (typeof window !== 'undefined' && window.innerWidth) || var agent = (typeof navigator !== 'undefined' && navigator.userAgent) || ""; -// server sources -var pid = (typeof process !== 'undefined' && process.pid) || 1; +if (nodeCrypto || + (typeof window !== "undefined" && + window.crypto && window.crypto.getRandomValues)) + Random = new RandomGenerator(); +else + Random = new RandomGenerator([new Date(), height, width, agent, Math.random()]); -// XXX On the server, use the crypto module (OpenSSL) instead of this PRNG. -// (Make Random.fraction be generated from Random.hexString instead of the -// other way around, and generate Random.hexString from crypto.randomBytes.) -Random = create([ - new Date(), height, width, agent, pid, Math.random() -]); +Random.create = function () { + return new RandomGenerator(arguments); +}; diff --git a/packages/random/random_tests.js b/packages/random/random_tests.js index 52e5b852e5..940afbb640 100644 --- a/packages/random/random_tests.js +++ b/packages/random/random_tests.js @@ -13,3 +13,17 @@ Tinytest.add('random', function (test) { test.equal(random.id(), "shxDnjWWmnKPEoLhM"); test.equal(random.id(), "6QTjB8C5SEqhmz4ni"); }); + +// node crypto and window.crypto.getRandomValues() don't let us specify a seed, +// but at least test that the output is in the right format. +Tinytest.add('random - format', function (test) { + var idLen = 17; + test.equal(Random.id().length, idLen); + var numDigits = 9; + var hexStr = Random.hexString(numDigits); + test.equal(hexStr.length, numDigits); + parseInt(hexStr, 16); // should not throw + var frac = Random.fraction(); + test.isTrue(frac < 1.0); + test.isTrue(frac >= 0.0); +}); diff --git a/packages/spark/spark_tests.js b/packages/spark/spark_tests.js index cab0f7ea94..e543ade932 100644 --- a/packages/spark/spark_tests.js +++ b/packages/spark/spark_tests.js @@ -1876,7 +1876,7 @@ Tinytest.add("spark - leaderboard, " + idGeneration, function(test) { })); var idGen; if (idGeneration === 'STRING') - idGen = Random.id; + idGen = _.bind(Random.id, Random); else idGen = function () { return new LocalCollection._ObjectID(); }; diff --git a/packages/test-helpers/seeded_random.js b/packages/test-helpers/seeded_random.js index 0094a20258..171a970486 100644 --- a/packages/test-helpers/seeded_random.js +++ b/packages/test-helpers/seeded_random.js @@ -3,7 +3,7 @@ SeededRandom = function(seed) { // seed may be a string or any type return new SeededRandom(seed); seed = seed || "seed"; - this.gen = new Random._Alea(seed); // from random.js + this.gen = Random.create(seed)._Alea; // from random.js }; SeededRandom.prototype.next = function() { return this.gen(); From 115164e4af84095e9399f4f04458906801ac8b85 Mon Sep 17 00:00:00 2001 From: Nicklas Ansman Giertz Date: Sat, 17 Aug 2013 18:35:40 -0700 Subject: [PATCH 14/35] Add the option to use a hosted domain with google's oauth The hd option is used to restrict which email domain that are allowed to log in to your app. Starting from this commit you can pass `hostedDomain: 'example.com'` to only allow emails from the domain `example.com`. --- packages/google/google_client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/google/google_client.js b/packages/google/google_client.js index 4f8c29c1a3..c99c376b1b 100644 --- a/packages/google/google_client.js +++ b/packages/google/google_client.js @@ -44,6 +44,10 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) '&access_type=' + accessType + '&approval_prompt=' + approvalPrompt; + if (options.hostedDomain) { + loginUrl += '&hd=' + encodeURIComponent(options.hostedDomain); + } + Oauth.initiateLogin(credentialToken, loginUrl, credentialRequestCompleteCallback, From 79d900cf65900c382bdc13d209e139a6b40e75aa Mon Sep 17 00:00:00 2001 From: David Glasser Date: Wed, 25 Sep 2013 22:34:56 -0700 Subject: [PATCH 15/35] Remove random from leaderboard and wordplay .meteor/packages. It's part of standard-app-packages (though it wasn't added to it until relatively late in the linker project). --- examples/leaderboard/.meteor/packages | 1 - examples/wordplay/.meteor/packages | 1 - 2 files changed, 2 deletions(-) diff --git a/examples/leaderboard/.meteor/packages b/examples/leaderboard/.meteor/packages index a5eb137152..240f048420 100644 --- a/examples/leaderboard/.meteor/packages +++ b/examples/leaderboard/.meteor/packages @@ -7,4 +7,3 @@ standard-app-packages autopublish insecure preserve-inputs -random diff --git a/examples/wordplay/.meteor/packages b/examples/wordplay/.meteor/packages index 1c4346821c..e695b93a6f 100644 --- a/examples/wordplay/.meteor/packages +++ b/examples/wordplay/.meteor/packages @@ -7,4 +7,3 @@ standard-app-packages insecure jquery preserve-inputs -random From a6db08ce8d14edf65e355c9f307b5b8eb8afad47 Mon Sep 17 00:00:00 2001 From: Matt DeBergalis Date: Wed, 25 Sep 2013 23:00:32 -0700 Subject: [PATCH 16/35] Improve the explanation of manual publisher functions. Fixes #1018. --- docs/client/api.html | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index 3aa992a512..e3f96a1f90 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -62,9 +62,9 @@ that Meteor will call each time a client subscribes to the name. Publish functions can return a [`Collection.Cursor`](#meteor_collection_cursor), in which case Meteor -will publish that cursor's documents. You can also return an array of -`Collection.Cursor`s, in which case Meteor will publish all of the -cursors. +will publish that cursor's documents to each subscribed client. You can +also return an array of `Collection.Cursor`s, in which case Meteor will +publish all of the cursors. {{#warning}} If you return multiple cursors in an array, they currently must all be from @@ -92,16 +92,15 @@ different collections. We hope to lift this restriction in a future release. ]; }); -Otherwise, the publish function should call the functions -[`added`](#publish_added) (when a new document is added to the published record -set), [`changed`](#publish_changed) (when some fields on a document in the -record set are changed or cleared), and [`removed`](#publish_removed) (when -documents are removed from the published record set) to inform subscribers about -documents. These methods are provided by `this` in your publish function. - - - - +Alternatively, a publish function can directly control its published +record set by calling the functions [`added`](#publish_added) (to add a +new document to the published record set), [`changed`](#publish_changed) +(to change or clear some fields on a document already in the published +record set), and [`removed`](#publish_removed) (to remove documents from +the published record set). Publish functions that use these functions +should also call [`ready`](#publish_ready) once the initial record set +is complete. These methods are provided by `this` in your publish +function. Example: From fcf6fdaa71803d9beb51d7c9965e356673df04cc Mon Sep 17 00:00:00 2001 From: Matt DeBergalis Date: Wed, 25 Sep 2013 23:28:15 -0700 Subject: [PATCH 17/35] Clean up docs for collections managed by remote DDP server. Fixes #875. --- docs/client/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/client/api.js b/docs/client/api.js index 1ca9cbce0f..f9f028d024 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -482,7 +482,7 @@ Template.api.meteor_collection = { options: [ {name: "connection", type: "Object", - descr: "The Meteor connection that will manage this collection. Uses the default connection if not specified. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection." + descr: "The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling [`DDP.connect`](#ddp_connect) to specify a different server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection." }, {name: "idGeneration", type: "String", From 0559491e875a64d773a4fdc61857f455e30117a3 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 26 Sep 2013 13:33:42 -0500 Subject: [PATCH 18/35] Improve Mongo compatibility with $ne/$nin/$not and arrays. Fixes #1451. We currently handle matches where the key has multiple values due to arrays in the document in two subtley different ways, depending on whether or not the array is in the last element of the keypath or not. (This is annoyingly necessary, at least in the current structure of how we compile selectors, due to various selectors differing in how they treat arrays.) The negative-style operators have "must match all values" semantics, but this was only being enforced when the branching was in the last element of the keypath. This commit semi-hackily applies those semantics for other branching too. We are still not 100% Mongo compatible (see XXX comment) but it's closer. --- packages/minimongo/minimongo_tests.js | 7 +++++++ packages/minimongo/selector.js | 22 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 07475e5c94..5f19e703e0 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -427,6 +427,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: {$ne: 1}}, {a: [1, 2]}); nomatch({a: {$ne: 2}}, {a: [1, 2]}); match({a: {$ne: 3}}, {a: [1, 2]}); + nomatch({'a.b': {$ne: 1}}, {a: [{b: 1}, {b: 2}]}); + nomatch({'a.b': {$ne: 2}}, {a: [{b: 1}, {b: 2}]}); + match({'a.b': {$ne: 3}}, {a: [{b: 1}, {b: 2}]}); nomatch({a: {$ne: {x: 1}}}, {a: {x: 1}}); match({a: {$ne: {x: 1}}}, {a: {x: 2}}); @@ -456,7 +459,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: {$nin: [1, 2, 3]}}, {a: [2]}); // tested against mongodb nomatch({a: {$nin: [{x: 1}, {x: 2}, {x: 3}]}}, {a: [{x: 2}]}); nomatch({a: {$nin: [1, 2, 3]}}, {a: [4, 2]}); + nomatch({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}, {b:2}]}); match({a: {$nin: [1, 2, 3]}}, {a: [4]}); + match({'a.b': {$nin: [1, 2, 3]}}, {a: [{b:4}]}); // $size match({a: {$size: 0}}, {a: []}); @@ -564,7 +569,9 @@ Tinytest.add("minimongo - selector_compiler", function (test) { match({x: {$not: {$lt: 10, $gt: 7}}}, {x: 6}); match({x: {$not: {$gt: 7}}}, {x: [2, 3, 4]}); + match({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}]}); nomatch({x: {$not: {$gt: 7}}}, {x: [2, 3, 4, 10]}); + nomatch({'x.y': {$not: {$gt: 7}}}, {x: [{y:2}, {y:3}, {y:4}, {y:10}]}); match({x: {$not: /a/}}, {x: "dog"}); nomatch({x: {$not: /a/}}, {x: "cat"}); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 7cf8fa0a5d..057e0d9195 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -551,9 +551,25 @@ var compileDocumentSelector = function (docSelector) { perKeySelectors.push(function (doc) { var branchValues = lookUpByIndex(doc); // We apply the selector to each "branched" value and return true if any - // match. This isn't 100% consistent with MongoDB; eg, see: - // https://jira.mongodb.org/browse/SERVER-8585 - return _.any(branchValues, valueSelectorFunc); + // match. However, for "negative" selectors like $ne or $not we actually + // require *all* elements to match. + // + // This is because {'x.tag': {$ne: "foo"}} applied to {x: [{tag: 'foo'}, + // {tag: 'bar'}]} should NOT match even though there is a branch that + // matches. (This matches the fact that $ne uses a negated + // _anyIfArrayPlus, for when the last level of the key is the array, + // which deMorgans into an 'all'.) + // + // XXX This isn't 100% consistent with MongoDB in 'null' cases: + // https://jira.mongodb.org/browse/SERVER-8585 + // XXX this still isn't right. consider {a: {$ne: 5, $gt: 6}}. the + // $ne needs to use the "all" logic and the $gt needs the "any" + // logic + var combiner = (subSelector && + (subSelector.$not || subSelector.$ne || + subSelector.$nin)) + ? _.all : _.any; + return combiner(branchValues, valueSelectorFunc); }); } }); From bad4c097c24eaf13d0045898c85b94669fc99003 Mon Sep 17 00:00:00 2001 From: Tim Haines Date: Tue, 24 Sep 2013 12:31:21 -0700 Subject: [PATCH 19/35] Add the error message to the Meteor._debug call --- packages/livedata/livedata_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/livedata/livedata_server.js b/packages/livedata/livedata_server.js index 11bd6752a5..b675dc40d6 100644 --- a/packages/livedata/livedata_server.js +++ b/packages/livedata/livedata_server.js @@ -1018,7 +1018,7 @@ Server = function () { } catch (e) { // XXX print stack nicely Meteor._debug("Internal exception while processing message", msg, - e.stack); + e.message, e.stack); } }); From c8308cdf9fcdf49bb91011e0b7ac9071a79f565d Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 26 Sep 2013 13:48:01 -0700 Subject: [PATCH 20/35] Turn off mongo http interface. This lets you run meteor on port 3000 and 4000 at the same time. --- tools/mongo_runner.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/mongo_runner.js b/tools/mongo_runner.js index c39e2e64cf..b2c1b55093 100644 --- a/tools/mongo_runner.js +++ b/tools/mongo_runner.js @@ -161,6 +161,7 @@ exports.launch_mongo = function (app_dir, port, launch_callback, on_exit_callbac var proc = child_process.spawn(mongod_path, [ '--bind_ip', '127.0.0.1', '--smallfiles', + '--nohttpinterface', '--port', port, '--dbpath', data_path ]); From d855e58f9220e8ee772c1b414777282291bff084 Mon Sep 17 00:00:00 2001 From: Slava Kim Date: Wed, 25 Sep 2013 20:41:37 -0700 Subject: [PATCH 21/35] Implement Accounts.config({ restrictCreationByEmail: 'mit.edu' }) - Check email for users created with password or any social account's email - Throw an error with explanation on bad email domain. - Set `hd` param for Google Accounts authentication url - Docs description - Touch History.md - Possibly should add it into QA process? --- History.md | 3 +++ docs/client/api.js | 5 ++++ packages/accounts-base/accounts_common.js | 5 ++-- packages/accounts-base/accounts_server.js | 28 +++++++++++++++++++++++ packages/google/google_client.js | 4 ++-- 5 files changed, 41 insertions(+), 4 deletions(-) diff --git a/History.md b/History.md index 3db302fed6..4154280e35 100644 --- a/History.md +++ b/History.md @@ -1,5 +1,8 @@ ## vNEXT +* `restrictCreationByEmail` option in `Accounts.config` to restrict new users to + emails of specific domain (eg. only users with @meteor.com emails). + * Better error when passing a string to {{#each}}. #722 * Write dates to Mongo as ISODate rather than Integer; existing data can be diff --git a/docs/client/api.js b/docs/client/api.js index 1ca9cbce0f..a75a564257 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1100,6 +1100,11 @@ Template.api.accounts_config = { name: "forbidClientAccountCreation", type: "Boolean", descr: "Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the \"Create account\" link will not be available." + }, + { + name: "restrictCreationByEmail", + type: "String", + descr: "If set, only allow new users with an email in the specified domain. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmail: 'school.edu' })`." } ] }; diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 1de58f1b2d..085591b128 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -34,8 +34,9 @@ Accounts._options = {}; Accounts.config = function(options) { // validate option keys var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", - "_tokenLifetimeSecs", "_tokenExpirationIntervalSecs", - "_minTokenLifetimeSecs", "_connectionCloseDelaySecs"]; + "restrictCreationByEmail", "_tokenLifetimeSecs", + "_tokenExpirationIntervalSecs", "_minTokenLifetimeSecs", + "_connectionCloseDelaySecs"]; _.each(_.keys(options), function (key) { if (!_.contains(VALID_KEYS, key)) { throw new Error("Accounts.config: Invalid key: " + key); diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index c97f0e17ab..c36bc5bf47 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -304,6 +304,34 @@ Accounts.validateNewUser = function (func) { validateNewUserHooks.push(func); }; +// Helper function: returns false if email does not match company domain from +// the configuration. +var testEmailDomain = function (email) { + var domain = Accounts._options.restrictCreationByEmail; + return !domain || (new RegExp('@' + domain + '$', 'i')).test(email); +}; + +// Validate new user's email or Google/Facebook/Github account's email +Accounts.validateNewUser(function (user) { + var domain = Accounts._options.restrictCreationByEmail; + if (!domain) + return true; + + var emailIsGood = true; + // User with password can have only one email on creation + if (user.emails) + emailIsGood &= testEmailDomain(user.emails[0].address); + + // Find any email of any service and check it + emailIsGood &= _.any(user.services, function (service) { + return service.email && testEmailDomain(service.email); + }); + + if (!emailIsGood) + throw new Meteor.Error(403, "@" + domain + " email required"); + + return true; +}); /// /// MANAGING USER OBJECTS diff --git a/packages/google/google_client.js b/packages/google/google_client.js index c99c376b1b..fce4e19750 100644 --- a/packages/google/google_client.js +++ b/packages/google/google_client.js @@ -44,8 +44,8 @@ Google.requestCredential = function (options, credentialRequestCompleteCallback) '&access_type=' + accessType + '&approval_prompt=' + approvalPrompt; - if (options.hostedDomain) { - loginUrl += '&hd=' + encodeURIComponent(options.hostedDomain); + if (Accounts._options.restrictCreationByEmail) { + loginUrl += '&hd=' + encodeURIComponent(Accounts._options.restrictCreationByEmail); } Oauth.initiateLogin(credentialToken, From 7bf11e35a6c1ca5996802a43b3a2b6c61475e031 Mon Sep 17 00:00:00 2001 From: Robert Lowe Date: Fri, 26 Jul 2013 23:43:20 -0400 Subject: [PATCH 22/35] Adds oauth1 improves to support requestTokenSecrets & dynamic urls. Atmosphere package: https://github.com/RobertLowe/meteor-accounts-trello Fixes #1167 Closes #1227 --- packages/oauth1/oauth1_binding.js | 28 +++++++++++++++++++--------- packages/oauth1/oauth1_server.js | 23 +++++++++++++++++------ packages/oauth1/oauth1_tests.js | 4 +++- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/packages/oauth1/oauth1_binding.js b/packages/oauth1/oauth1_binding.js index 8047f387c6..59b6fdb5d4 100644 --- a/packages/oauth1/oauth1_binding.js +++ b/packages/oauth1/oauth1_binding.js @@ -4,19 +4,18 @@ var querystring = Npm.require("querystring"); // An OAuth1 wrapper around http calls which helps get tokens and // takes care of HTTP headers // -// @param consumerKey {String} As supplied by the OAuth1 provider -// @param consumerSecret {String} As supplied by the OAuth1 provider +// @param config {Object} Keys, Secrets, etc // @param urls {Object} // - requestToken (String): url // - authorize (String): url // - accessToken (String): url // - authenticate (String): url -OAuth1Binding = function(consumerKey, consumerSecret, urls) { - this._consumerKey = consumerKey; - this._secret = consumerSecret; +OAuth1Binding = function(config, urls) { + this._config = config; this._urls = urls; }; + OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { var self = this; @@ -27,15 +26,20 @@ OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { var response = self._call('POST', self._urls.requestToken, headers); var tokens = querystring.parse(response.content); - // XXX should we also store oauth_token_secret here? + // XXX should we also store oauth_token_secret here? Yes, we should. if (!tokens.oauth_callback_confirmed) throw new Error("oauth_callback_confirmed false when requesting oauth1 token", tokens); self.requestToken = tokens.oauth_token; + self.requestTokenSecret = tokens.oauth_token_secret; }; -OAuth1Binding.prototype.prepareAccessToken = function(query) { +OAuth1Binding.prototype.prepareAccessToken = function(query, requestTokenSecret) { var self = this; + // support implemntations that use request token secrets + if (requestTokenSecret) + self.accessTokenSecret = requestTokenSecret + var headers = self._buildHeader({ oauth_token: query.oauth_token }); @@ -76,7 +80,7 @@ OAuth1Binding.prototype.post = function(url, params, callback) { OAuth1Binding.prototype._buildHeader = function(headers) { var self = this; return _.extend({ - oauth_consumer_key: self._consumerKey, + oauth_consumer_key: self._config.consumerKey, oauth_nonce: Random.id().replace(/\W/g, ''), oauth_signature_method: 'HMAC-SHA1', oauth_timestamp: (new Date().valueOf()/1000).toFixed().toString(), @@ -98,7 +102,7 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access self._encodeString(parameters) ].join('&'); - var signingKey = self._encodeString(self._secret) + '&'; + var signingKey = self._encodeString(self._config.secret) + '&'; if (accessTokenSecret) signingKey += self._encodeString(accessTokenSecret); @@ -108,6 +112,12 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access OAuth1Binding.prototype._call = function(method, url, headers, params, callback) { var self = this; + + // callback functions to support parameters/customization + if(typeof(url) == "function"){ + url = url(self); + } + // Get the signature headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret, params); diff --git a/packages/oauth1/oauth1_server.js b/packages/oauth1/oauth1_server.js index 2e2a530020..b4a6ab5f1c 100644 --- a/packages/oauth1/oauth1_server.js +++ b/packages/oauth1/oauth1_server.js @@ -1,5 +1,6 @@ // A place to store request tokens pending verification var requestTokens = {}; +var querystring = Npm.require("querystring"); OAuth1Test = {requestTokens: requestTokens}; @@ -12,8 +13,7 @@ Oauth._requestHandlers['1'] = function (service, query, res) { } var urls = service.urls; - var oauthBinding = new OAuth1Binding( - config.consumerKey, config.secret, urls); + var oauthBinding = new OAuth1Binding(config, urls); if (query.requestTokenAndRedirect) { // step 1 - get and store a request token @@ -22,10 +22,20 @@ Oauth._requestHandlers['1'] = function (service, query, res) { oauthBinding.prepareRequestToken(query.requestTokenAndRedirect); // Keep track of request token so we can verify it on the next step - requestTokens[query.state] = oauthBinding.requestToken; + requestTokens[query.state] = { + requestToken: oauthBinding.requestToken, + requestTokenSecret: oauthBinding.requestTokenSecret + }; + + // support for scope/name parameters + var redirectUrl = undefined; + if(typeof(urls.authenticate) == "function"){ + redirectUrl = urls.authenticate(oauthBinding); + } else { + redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; + } // redirect to provider login, which will redirect back to "step 2" below - var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; res.writeHead(302, {'Location': redirectUrl}); res.end(); } else { @@ -34,7 +44,8 @@ Oauth._requestHandlers['1'] = function (service, query, res) { // token and access token secret and log in as user // Get the user's request token so we can verify it and clear it - var requestToken = requestTokens[query.state]; + var requestToken = requestTokens[query.state].requestToken; + var requestTokenSecret = requestTokens[query.state].requestTokenSecret; delete requestTokens[query.state]; // Verify user authorized access and the oauth_token matches @@ -45,7 +56,7 @@ Oauth._requestHandlers['1'] = function (service, query, res) { // subsequent call to the `login` method will be immediate. // Get the access token for signing requests - oauthBinding.prepareAccessToken(query); + oauthBinding.prepareAccessToken(query, requestTokenSecret); // Run service-specific handler. var oauthResult = service.handleOauthRequest(oauthBinding); diff --git a/packages/oauth1/oauth1_tests.js b/packages/oauth1/oauth1_tests.js index d7eb0e1979..272e0cb123 100644 --- a/packages/oauth1/oauth1_tests.js +++ b/packages/oauth1/oauth1_tests.js @@ -40,7 +40,9 @@ Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test) }); // simulate logging in using twitterfoo - OAuth1Test.requestTokens[credentialToken] = twitterfooAccessToken; + OAuth1Test.requestTokens[credentialToken] = { + requestToken: twitterfooAccessToken + }; var req = { method: "POST", From 9e0897aecd41109e71c8c148364b4f9bd1b4ccb6 Mon Sep 17 00:00:00 2001 From: Robert Lowe Date: Fri, 30 Aug 2013 16:56:53 -0400 Subject: [PATCH 23/35] Adds documentation for `config` arg of `OAuth1Binding`'s constructor Context: https://github.com/meteor/meteor/pull/1253#commitcomment-3980200 --- packages/oauth1/oauth1_binding.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/oauth1/oauth1_binding.js b/packages/oauth1/oauth1_binding.js index 59b6fdb5d4..c3af432318 100644 --- a/packages/oauth1/oauth1_binding.js +++ b/packages/oauth1/oauth1_binding.js @@ -4,7 +4,9 @@ var querystring = Npm.require("querystring"); // An OAuth1 wrapper around http calls which helps get tokens and // takes care of HTTP headers // -// @param config {Object} Keys, Secrets, etc +// @param config {Object} +// - consumerKey (String): oauth consumer key +// - secret (String): oauth consumer secret // @param urls {Object} // - requestToken (String): url // - authorize (String): url @@ -15,7 +17,6 @@ OAuth1Binding = function(config, urls) { this._urls = urls; }; - OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { var self = this; From 400778a55960f67de4178f0916fe166bd5666c26 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Wed, 25 Sep 2013 20:16:19 -0700 Subject: [PATCH 24/35] Cleanup. Whitespace, comments, style, unused variables. --- packages/oauth1/oauth1_binding.js | 22 ++++++++++++++-------- packages/oauth1/oauth1_server.js | 13 ++++++------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/oauth1/oauth1_binding.js b/packages/oauth1/oauth1_binding.js index c3af432318..bf9b88140d 100644 --- a/packages/oauth1/oauth1_binding.js +++ b/packages/oauth1/oauth1_binding.js @@ -27,9 +27,10 @@ OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { var response = self._call('POST', self._urls.requestToken, headers); var tokens = querystring.parse(response.content); - // XXX should we also store oauth_token_secret here? Yes, we should. if (!tokens.oauth_callback_confirmed) - throw new Error("oauth_callback_confirmed false when requesting oauth1 token", tokens); + throw new Error( + "oauth_callback_confirmed false when requesting oauth1 token", tokens); + self.requestToken = tokens.oauth_token; self.requestTokenSecret = tokens.oauth_token_secret; }; @@ -37,9 +38,14 @@ OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) { OAuth1Binding.prototype.prepareAccessToken = function(query, requestTokenSecret) { var self = this; - // support implemntations that use request token secrets + // support implementations that use request token secrets. This is + // read by self._call. + // + // XXX make it a param to call, not something stashed on self? It's + // kinda confusing right now, everything except this is passed as + // arguments, but this is stored. if (requestTokenSecret) - self.accessTokenSecret = requestTokenSecret + self.accessTokenSecret = requestTokenSecret; var headers = self._buildHeader({ oauth_token: query.oauth_token @@ -113,14 +119,14 @@ OAuth1Binding.prototype._getSignature = function(method, url, rawHeaders, access OAuth1Binding.prototype._call = function(method, url, headers, params, callback) { var self = this; - - // callback functions to support parameters/customization - if(typeof(url) == "function"){ + // all URLs to be functions to support parameters/customization + if(typeof url === "function") { url = url(self); } // Get the signature - headers.oauth_signature = self._getSignature(method, url, headers, self.accessTokenSecret, params); + headers.oauth_signature = + self._getSignature(method, url, headers, self.accessTokenSecret, params); // Make a authorization string according to oauth1 spec var authString = self._getAuthHeaderString(headers); diff --git a/packages/oauth1/oauth1_server.js b/packages/oauth1/oauth1_server.js index b4a6ab5f1c..6a61fbd6c7 100644 --- a/packages/oauth1/oauth1_server.js +++ b/packages/oauth1/oauth1_server.js @@ -1,6 +1,5 @@ // A place to store request tokens pending verification var requestTokens = {}; -var querystring = Npm.require("querystring"); OAuth1Test = {requestTokens: requestTokens}; @@ -29,7 +28,7 @@ Oauth._requestHandlers['1'] = function (service, query, res) { // support for scope/name parameters var redirectUrl = undefined; - if(typeof(urls.authenticate) == "function"){ + if(typeof urls.authenticate === "function") { redirectUrl = urls.authenticate(oauthBinding); } else { redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; @@ -44,7 +43,7 @@ Oauth._requestHandlers['1'] = function (service, query, res) { // token and access token secret and log in as user // Get the user's request token so we can verify it and clear it - var requestToken = requestTokens[query.state].requestToken; + var requestToken = requestTokens[query.state].requestToken; var requestTokenSecret = requestTokens[query.state].requestTokenSecret; delete requestTokens[query.state]; @@ -63,10 +62,10 @@ Oauth._requestHandlers['1'] = function (service, query, res) { // Add the login result to the result map Oauth._loginResultForCredentialToken[query.state] = { - serviceName: service.serviceName, - serviceData: oauthResult.serviceData, - options: oauthResult.options - }; + serviceName: service.serviceName, + serviceData: oauthResult.serviceData, + options: oauthResult.options + }; } // Either close the window, redirect, or render nothing From eda50d2d1ed65ff40f0b85f8dc089d2c50ff6dd9 Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Fri, 27 Sep 2013 14:38:47 -0700 Subject: [PATCH 25/35] add history.md note --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 4154280e35..c8c64063d2 100644 --- a/History.md +++ b/History.md @@ -21,6 +21,9 @@ running on Linux machines with glibc 2.9 or newer (Ubuntu 10.04+, RHEL and CentOS 6+, Fedora 10+, Debian 6+). +* Support OAuth1 services that require request token secrets as well as + authentication token secrets. #1253 + ## v0.6.5.1 * Fix syntax errors on lines that end with a backslash. #1326 From 3177d9ad416ae97a98a2b8c4b2b40a9fc03f6b9c Mon Sep 17 00:00:00 2001 From: Nick Martin Date: Thu, 26 Sep 2013 19:42:16 -0700 Subject: [PATCH 26/35] Use http_proxy environment variable so meteor update and deploy work behind an http proxy. --- History.md | 5 +++ tools/deploy-galaxy.js | 7 ++-- tools/deploy.js | 23 +++++----- tools/files.js | 53 ----------------------- tools/http-helpers.js | 95 ++++++++++++++++++++++++++++++++++++++++++ tools/meteor_npm.js | 3 +- tools/updater.js | 3 +- tools/warehouse.js | 9 ++-- 8 files changed, 125 insertions(+), 73 deletions(-) create mode 100644 tools/http-helpers.js diff --git a/History.md b/History.md index c8c64063d2..6c2d8769ba 100644 --- a/History.md +++ b/History.md @@ -17,6 +17,11 @@ 0.6.5. (A bug prevented the 0.6.5 reimplementation of `register_extension` from working properly anyway.) +* Support using an HTTP proxy in the `meteor` command line tool. This + allows the `update`, `deploy`, `logs`, and `mongo` commands to work + behind a proxy. Use the standard `http_proxy` environment variable to + specify your proxy endpoint. #429, #689, #1338 + * Build Linux binaries on an older Linux machine. Meteor now supports running on Linux machines with glibc 2.9 or newer (Ubuntu 10.04+, RHEL and CentOS 6+, Fedora 10+, Debian 6+). diff --git a/tools/deploy-galaxy.js b/tools/deploy-galaxy.js index 12efda1bb2..228fd616ec 100644 --- a/tools/deploy-galaxy.js +++ b/tools/deploy-galaxy.js @@ -5,7 +5,7 @@ var fs = require('fs'); var unipackage = require('./unipackage.js'); var fiberHelpers = require('./fiber-helpers.js'); var Fiber = require('fibers'); -var request = require('request'); +var httpHelpers = require('./http-helpers.js'); var _ = require('underscore'); // a bit of a hack @@ -90,7 +90,7 @@ exports.discoverGalaxy = function (app) { // At some point we may want to send a version in the request so that galaxy // can respond differently to different versions of meteor. - request({ + httpHelpers.request({ url: url, json: true, strictSSL: true, @@ -210,7 +210,8 @@ exports.deploy = function (options) { var fileSize = fs.statSync(starball).size; var fileStream = fs.createReadStream(starball); var future = new Future; - var req = request.put({ + var req = httpHelpers.request({ + method: "PUT", url: info.put, headers: { 'content-length': fileSize, 'content-type': 'application/octet-stream' }, diff --git a/tools/deploy.js b/tools/deploy.js index 90e00d0811..caf8ccc627 100644 --- a/tools/deploy.js +++ b/tools/deploy.js @@ -7,6 +7,7 @@ var qs = require('querystring'); var path = require('path'); var files = require('./files.js'); +var httpHelpers = require('./http-helpers.js'); var warehouse = require('./warehouse.js'); var buildmessage = require('./buildmessage.js'); var _ = require('underscore'); @@ -43,15 +44,16 @@ var meteor_rpc = function (rpc_name, method, site, query_params, callback) { url += '?' + qs.stringify(query_params); } - var request = require('request'); - var r = request({method: method, url: url}, function (error, response, body) { - if (error || ((response.statusCode !== 200) - && (response.statusCode !== 201))) - // pass some non-falsy error back to callback - callback(error || response.statusCode, body); - else - callback(null, body); - }); + var r = httpHelpers.request( + {method: method, url: url}, + function (error, response, body) { + if (error || ((response.statusCode !== 200) + && (response.statusCode !== 201))) + // pass some non-falsy error back to callback + callback(error || response.statusCode, body); + else + callback(null, body); + }); return r; }; @@ -345,8 +347,7 @@ var with_password = function (site, callback) { // Future.throw. Basically, what Future.wrap does. callback = inFiber(callback); - var request = require('request'); - request(check_url, function (error, response, body) { + httpHelpers.request(check_url, function (error, response, body) { if (error || response.statusCode !== 200) { callback(); diff --git a/tools/files.js b/tools/files.js index 935051ad60..e14b9a311c 100644 --- a/tools/files.js +++ b/tools/files.js @@ -436,59 +436,6 @@ _.extend(exports, { future.wait(); }, - // A synchronous wrapper around request(...) that returns the response "body" - // or throws. - getUrl: function (urlOrOptions, callback) { - var future = new Future; - // can't just use Future.wrap, because we want to return "body", not - // "response". - - urlOrOptions = _.clone(urlOrOptions); // we are going to change it - var appVersion; - try { - appVersion = getToolsVersion(); - } catch(e) { - appVersion = 'checkout'; - } - - // meteorReleaseContext - an option with information about app directory - // release versions, etc, is used to get exact Meteor version used. - if (urlOrOptions.hasOwnProperty('meteorReleaseContext')) { - // Get meteor app release version: if specified in command line args, take - // releaseVersion, if not specified, try global meteor version - var meteorReleaseContext = urlOrOptions.meteorReleaseContext; - appVersion = meteorReleaseContext.releaseVersion; - - if (appVersion === 'none') - appVersion = meteorReleaseContext.appReleaseVersion; - if (appVersion === 'none') - appVersion = 'checkout'; - - delete urlOrOptions.meteorReleaseContext; - } - - // Get some kind of User Agent: environment information. - var ua = util.format('Meteor/%s OS/%s (%s; %s; %s;)', - appVersion, os.platform(), os.type(), os.release(), os.arch()); - - var headers = {'User-Agent': ua }; - - if (_.isObject(urlOrOptions)) - urlOrOptions.headers = _.extend(headers, urlOrOptions.headers); - else - urlOrOptions = { url: urlOrOptions, headers: headers }; - - var request = require('request'); - request(urlOrOptions, function (error, response, body) { - if (error) - future.throw(new files.OfflineError(error)); - else if (response.statusCode >= 400 && response.statusCode < 600) - future.throw(response); - else - future.return(body); - }); - return future.wait(); - }, // Use this if you'd like to replace a directory with another directory as // close to atomically as possible. It's better than recursively deleting the diff --git a/tools/http-helpers.js b/tools/http-helpers.js new file mode 100644 index 0000000000..68e8279a56 --- /dev/null +++ b/tools/http-helpers.js @@ -0,0 +1,95 @@ +/// +/// utility functions for dealing with urls and http +/// + +var os = require('os'); +var util = require('util'); + +var _ = require('underscore'); +var request = require('request'); +var Future = require('fibers/future'); + +var files = require('./files.js'); + + +var httpHelpers = exports; +_.extend(exports, { + + // A wrapper around request that sets http proxy. + request: function (urlOrOptions, callback) { + + if (!_.isObject(urlOrOptions)) + urlOrOptions = { url: urlOrOptions }; + + var url = urlOrOptions.url; + + // try to get proxy from environment + var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null; + // if we're going to an https url, try the https_proxy env variable first. + if (/^https/i.test(url)) { + proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy; + } + if (proxy && !urlOrOptions.proxy) { + urlOrOptions.proxy = proxy; + } + + return request(urlOrOptions, callback); + }, + + + + // A synchronous wrapper around request(...) that returns the response "body" + // or throws. + getUrl: function (urlOrOptions, callback) { + var future = new Future; + // can't just use Future.wrap, because we want to return "body", not + // "response". + + urlOrOptions = _.clone(urlOrOptions); // we are going to change it + var appVersion; + try { + appVersion = files.getToolsVersion(); + } catch(e) { + appVersion = 'checkout'; + } + + // meteorReleaseContext - an option with information about app directory + // release versions, etc, is used to get exact Meteor version used. + if (urlOrOptions.hasOwnProperty('meteorReleaseContext')) { + // Get meteor app release version: if specified in command line args, take + // releaseVersion, if not specified, try global meteor version + var meteorReleaseContext = urlOrOptions.meteorReleaseContext; + appVersion = meteorReleaseContext.releaseVersion; + + if (appVersion === 'none') + appVersion = meteorReleaseContext.appReleaseVersion; + if (appVersion === 'none') + appVersion = 'checkout'; + + delete urlOrOptions.meteorReleaseContext; + } + + // Get some kind of User Agent: environment information. + var ua = util.format('Meteor/%s OS/%s (%s; %s; %s;)', + appVersion, os.platform(), os.type(), os.release(), os.arch()); + + var headers = {'User-Agent': ua }; + + if (_.isObject(urlOrOptions)) + urlOrOptions.headers = _.extend(headers, urlOrOptions.headers); + else + urlOrOptions = { url: urlOrOptions, headers: headers }; + + httpHelpers.request(urlOrOptions, function (error, response, body) { + if (error) + future.throw(new files.OfflineError(error)); + else if (response.statusCode >= 400 && response.statusCode < 600) + future.throw(response); + else + future.return(body); + }); + return future.wait(); + } + + +}); diff --git a/tools/meteor_npm.js b/tools/meteor_npm.js index 67fb840d33..b70a6c3ca2 100644 --- a/tools/meteor_npm.js +++ b/tools/meteor_npm.js @@ -10,6 +10,7 @@ var path = require('path'); var fs = require('fs'); var cleanup = require(path.join(__dirname, 'cleanup.js')); var files = require(path.join(__dirname, 'files.js')); +var httpHelpers = require('./http-helpers.js'); var buildmessage = require('./buildmessage.js'); var _ = require('underscore'); @@ -491,7 +492,7 @@ _.extend(exports, { // dependencies. `npm install` times out after more than a minute. _ensureConnected: function () { try { - files.getUrl("http://registry.npmjs.org"); + httpHelpers.getUrl("http://registry.npmjs.org"); } catch (e) { buildmessage.error("Can't install npm dependencies. " + "Are you connected to the internet?"); diff --git a/tools/updater.js b/tools/updater.js index 0369ba5ee9..368c95452c 100644 --- a/tools/updater.js +++ b/tools/updater.js @@ -6,6 +6,7 @@ var testingUpdater = false; var inFiber = require('./fiber-helpers.js').inFiber; var files = require('./files.js'); var warehouse = require('./warehouse.js'); +var httpHelpers = require('./http-helpers.js'); var manifestUrl = testingUpdater ? 'https://s3.amazonaws.com/com.meteor.static/test/update/manifest.json' @@ -21,7 +22,7 @@ exports.getManifest = function (context) { if (context) options.meteorReleaseContext = context; - return files.getUrl(options); + return httpHelpers.getUrl(options); }; exports.startUpdateChecks = function (context) { diff --git a/tools/warehouse.js b/tools/warehouse.js index 91e47b3311..d2ea01838e 100644 --- a/tools/warehouse.js +++ b/tools/warehouse.js @@ -28,6 +28,7 @@ var _ = require("underscore"); var files = require('./files.js'); var updater = require('./updater.js'); +var httpHelpers = require('./http-helpers.js'); var fiberHelpers = require('./fiber-helpers.js'); var logging = require('./logging.js'); @@ -235,7 +236,7 @@ _.extend(warehouse, { // after we're done writing packages if (!releaseAlreadyExists) { try { - releaseManifestText = files.getUrl( + releaseManifestText = httpHelpers.getUrl( WAREHOUSE_URLBASE + "/releases/" + releaseVersion + ".release.json"); } catch (e) { // just throw, if we're in the background anyway, or if this is the @@ -303,7 +304,7 @@ _.extend(warehouse, { // try getting the releases's notices. only blessed releases have one, so // if we can't find it just proceed. try { - var notices = files.getUrl( + var notices = httpHelpers.getUrl( WAREHOUSE_URLBASE + "/releases/" + releaseVersion + ".notices.json"); // Real notices are valid JSON. @@ -347,7 +348,7 @@ _.extend(warehouse, { "meteor-tools-" + toolsVersion + "-" + platform + ".tar.gz"; var toolsTarballPath = "/tools/" + toolsVersion + "/" + toolsTarballFilename; - var toolsTarball = files.getUrl({ + var toolsTarball = httpHelpers.getUrl({ url: WAREHOUSE_URLBASE + toolsTarballPath, encoding: null }); @@ -430,7 +431,7 @@ _.extend(warehouse, { "/" + version + "/" + name + '-' + version + "-" + platform + ".tar.gz"; - var tarball = files.getUrl({url: packageUrl, encoding: null}); + var tarball = httpHelpers.getUrl({url: packageUrl, encoding: null}); files.extractTarGz(tarball, packageDir); if (!dontWriteFreshFile) fs.writeFileSync(warehouse.getPackageFreshFile(name, version), ''); From d4d7ebb78318ff74a45d5c08662ca08091654560 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 23 Sep 2013 16:35:37 -0700 Subject: [PATCH 27/35] Implements ES5-style callbacks for cursor forEach and map. Fixes #63. Based on iwoj's PR. Needs tests and docs. --- packages/minimongo/minimongo.js | 14 +++--- packages/mongo-livedata/mongo_driver.js | 60 ++++++++++++++----------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/packages/minimongo/minimongo.js b/packages/minimongo/minimongo.js index 7fb03f7810..7a00b71e2d 100644 --- a/packages/minimongo/minimongo.js +++ b/packages/minimongo/minimongo.js @@ -140,9 +140,8 @@ LocalCollection.prototype.findOne = function (selector, options) { return this.find(selector, options).fetch()[0]; }; -LocalCollection.Cursor.prototype.forEach = function (callback) { +LocalCollection.Cursor.prototype.forEach = function (callback, thisArg) { var self = this; - var doc; if (self.db_objects === null) self.db_objects = self._getRawObjects(true); @@ -155,12 +154,13 @@ LocalCollection.Cursor.prototype.forEach = function (callback) { movedBefore: true}); while (self.cursor_pos < self.db_objects.length) { - var elt = EJSON.clone(self.db_objects[self.cursor_pos++]); + var elt = EJSON.clone(self.db_objects[self.cursor_pos]); if (self.projection_f) elt = self.projection_f(elt); if (self._transform) elt = self._transform(elt); - callback(elt); + callback.call(thisArg, elt, self.cursor_pos, self); + ++self.cursor_pos; } }; @@ -169,11 +169,11 @@ LocalCollection.Cursor.prototype.getTransform = function () { return self._transform; }; -LocalCollection.Cursor.prototype.map = function (callback) { +LocalCollection.Cursor.prototype.map = function (callback, thisArg) { var self = this; var res = []; - self.forEach(function (doc) { - res.push(callback(doc)); + self.forEach(function (doc, index) { + res.push(callback.call(thisArg, doc, index, self)); }); return res; }; diff --git a/packages/mongo-livedata/mongo_driver.js b/packages/mongo-livedata/mongo_driver.js index deda9b9d48..aaf9d3022f 100644 --- a/packages/mongo-livedata/mongo_driver.js +++ b/packages/mongo-livedata/mongo_driver.js @@ -437,9 +437,15 @@ _.each(['forEach', 'map', 'rewind', 'fetch', 'count'], function (method) { if (self._cursorDescription.options.tailable) throw new Error("Cannot call " + method + " on a tailable cursor"); - if (!self._synchronousCursor) + if (!self._synchronousCursor) { self._synchronousCursor = self._mongo._createSynchronousCursor( - self._cursorDescription, true); + self._cursorDescription, { + // Make sure that the "self" argument to forEach/map callbacks is the + // Cursor, not the SynchronousCursor. + selfForIteration: self, + useTransform: true + }); + } return self._synchronousCursor[method].apply( self._synchronousCursor, arguments); @@ -481,20 +487,21 @@ Cursor.prototype.observeChanges = function (callbacks) { self._cursorDescription, ordered, callbacks); }; -MongoConnection.prototype._createSynchronousCursor = function(cursorDescription, - useTransform) { +MongoConnection.prototype._createSynchronousCursor = function( + cursorDescription, options) { var self = this; + options = _.pick(options || {}, 'selfForIteration', 'useTransform'); var collection = self._getCollection(cursorDescription.collectionName); - var options = cursorDescription.options; + var cursorOptions = cursorDescription.options; var mongoOptions = { - sort: options.sort, - limit: options.limit, - skip: options.skip + sort: cursorOptions.sort, + limit: cursorOptions.limit, + skip: cursorOptions.skip }; // Do we want a tailable cursor (which only works on capped collections)? - if (options.tailable) { + if (cursorOptions.tailable) { // We want a tailable cursor... mongoOptions.tailable = true; // ... and for the server to wait a bit if any getMore has no data (rather @@ -507,16 +514,21 @@ MongoConnection.prototype._createSynchronousCursor = function(cursorDescription, var dbCursor = collection.find( replaceTypes(cursorDescription.selector, replaceMeteorAtomWithMongo), - options.fields, mongoOptions); + cursorOptions.fields, mongoOptions); - return new SynchronousCursor(dbCursor, cursorDescription, useTransform); + return new SynchronousCursor(dbCursor, cursorDescription, options); }; -var SynchronousCursor = function (dbCursor, cursorDescription, useTransform) { +var SynchronousCursor = function (dbCursor, cursorDescription, options) { var self = this; + options = _.pick(options || {}, 'selfForIteration', 'useTransform'); + self._dbCursor = dbCursor; self._cursorDescription = cursorDescription; - if (useTransform && cursorDescription.options.transform) { + // The "self" argument passed to forEach/map callbacks. If we're wrapped + // inside a user-visible Cursor, we want to provide the outer cursor! + self._selfForIteration = options.selfForIteration || self; + if (options.useTransform && cursorDescription.options.transform) { self._transform = Deps._makeNonreactive( cursorDescription.options.transform ); @@ -558,29 +570,26 @@ _.extend(SynchronousCursor.prototype, { } }, - // XXX Make more like ECMA forEach: - // https://github.com/meteor/meteor/pull/63#issuecomment-5320050 - forEach: function (callback) { + forEach: function (callback, thisArg) { var self = this; // We implement the loop ourself instead of using self._dbCursor.each, // because "each" will call its callback outside of a fiber which makes it // much more complex to make this function synchronous. + var index = 0; while (true) { var doc = self._nextObject(); if (!doc) return; - callback(doc); + callback.call(thisArg, doc, index++, self._selfForIteration); } }, - // XXX Make more like ECMA map: - // https://github.com/meteor/meteor/pull/63#issuecomment-5320050 // XXX Allow overlapping callback executions if callback yields. - map: function (callback) { + map: function (callback, thisArg) { var self = this; var res = []; - self.forEach(function (doc) { - res.push(callback(doc)); + self.forEach(function (doc, index) { + res.push(callback.call(thisArg, doc, index, self._selfForIteration)); }); return res; }, @@ -892,7 +901,7 @@ _.extend(LiveResultsSet.prototype, { self._synchronousCursor.rewind(); } else { self._synchronousCursor = self._mongoHandle._createSynchronousCursor( - self._cursorDescription, false /* !useTransform */); + self._cursorDescription); } var newResults = self._synchronousCursor.getRawObjects(self._ordered); var oldResults = self._results; @@ -1020,8 +1029,7 @@ MongoConnection.prototype._observeChangesTailable = function ( + " tailable cursor without a " + (ordered ? "addedBefore" : "added") + " callback"); } - var cursor = self._createSynchronousCursor(cursorDescription, - false /* useTransform */); + var cursor = self._createSynchronousCursor(cursorDescription); var stopped = false; var lastTS = undefined; @@ -1063,7 +1071,7 @@ MongoConnection.prototype._observeChangesTailable = function ( cursor = self._createSynchronousCursor(new CursorDescription( cursorDescription.collectionName, newSelector, - cursorDescription.options), false /* useTransform */); + cursorDescription.options)); } } }); From 3e1afb78501dcade1a57c71d7acc821939ee8122 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Mon, 23 Sep 2013 17:44:34 -0700 Subject: [PATCH 28/35] Tests for extra forEach/map arguments. --- packages/minimongo/minimongo_tests.js | 15 ++++++++++++--- .../mongo-livedata/mongo_livedata_tests.js | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 5f19e703e0..6248c99825 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -180,16 +180,25 @@ Tinytest.add("minimongo - cursors", function (test) { // forEach var count = 0; - q.forEach(function (obj) { + var context = {}; + q.forEach(function (obj, i, cursor) { test.equal(obj.i, count++); - }); + test.equal(obj.i, i); + test.isTrue(context === this); + test.isTrue(cursor === q); + }, context); test.equal(count, 20); // everything empty test.length(q.fetch(), 0); q.rewind(); // map - res = q.map(function (obj) { return obj.i * 2; }); + res = q.map(function (obj, i, cursor) { + test.equal(obj.i, i); + test.isTrue(context === this); + test.isTrue(cursor === q); + return obj.i * 2; + }, context); test.length(res, 20); for (var i = 0; i < 20; i++) test.equal(res[i], i * 2); diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index 64a23991ee..4b8a75fdf8 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -176,7 +176,12 @@ Tinytest.addAsync("mongo-livedata - basics, " + idGeneration, function (test, on var cur = coll.find({run: run}, {sort: ["x"]}); var total = 0; - cur.forEach(function (doc) { + var index = 0; + var context = {}; + cur.forEach(function (doc, i, cursor) { + test.equal(i, index++); + test.isTrue(cursor === cur); + test.isTrue(context === this); total *= 10; if (Meteor.isServer) { // Verify that the callbacks from forEach run sequentially and that @@ -190,13 +195,19 @@ Tinytest.addAsync("mongo-livedata - basics, " + idGeneration, function (test, on total += doc.x; // verify the meteor environment is set up here coll2.insert({total:total}); - }); + }, context); test.equal(total, 14); cur.rewind(); - test.equal(cur.map(function (doc) { + index = 0; + test.equal(cur.map(function (doc, i, cursor) { + // XXX we could theoretically make map run its iterations in parallel or + // something which would make this fail + test.equal(i, index++); + test.isTrue(cursor === cur); + test.isTrue(context === this); return doc.x * 2; - }), [2, 8]); + }, context), [2, 8]); test.equal(_.pluck(coll.find({run: run}, {sort: {x: -1}}).fetch(), "x"), [4, 1]); From 7f490a4081c70fe504567826a3b4884b691e21c8 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 24 Sep 2013 16:08:40 -0700 Subject: [PATCH 29/35] Docs for ES5-style iteration. --- docs/client/api.html | 4 ++++ docs/client/api.js | 14 ++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/client/api.html b/docs/client/api.html index e3f96a1f90..9fa58c4fbb 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -965,6 +965,8 @@ cursor, use [`forEach`](#foreach), [`map`](#map), or [`fetch`](#fetch). {{> api_box cursor_foreach}} +This interface is compatible with [Array.forEach](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach). + When called from a reactive computation, `forEach` registers dependencies on the matching documents. @@ -980,6 +982,8 @@ Examples: {{> api_box cursor_map}} +This interface is compatible with [Array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map). + When called from a reactive computation, `map` registers dependencies on the matching documents. diff --git a/docs/client/api.js b/docs/client/api.js index 2ab15f343e..4a7402824a 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -675,25 +675,31 @@ Template.api.cursor_fetch = { Template.api.cursor_foreach = { id: "foreach", - name: "cursor.forEach(callback)", + name: "cursor.forEach(callback, [thisArg])", locus: "Anywhere", descr: ["Call `callback` once for each matching document, sequentially and synchronously."], args: [ {name: "callback", type: "Function", - descr: "Function to call."} + descr: "Function to call. It will be called with three arguments: the document, a 0-based index, and cursor itself."}, + {name: "thisArg", + type: "Any", + descr: "An object which will be the value of `this` inside `callback`."} ] }; Template.api.cursor_map = { id: "map", - name: "cursor.map(callback)", + name: "cursor.map(callback, [thisArg])", locus: "Anywhere", descr: ["Map callback over all matching documents. Returns an Array."], args: [ {name: "callback", type: "Function", - descr: "Function to call."} + descr: "Function to call. It will be called with three arguments: the document, a 0-based index, and cursor itself."}, + {name: "thisArg", + type: "Any", + descr: "An object which will be the value of `this` inside `callback`."} ] }; From b86ba7e3dad23411559a29ebab4c678ae97f393c Mon Sep 17 00:00:00 2001 From: David Glasser Date: Tue, 24 Sep 2013 16:11:29 -0700 Subject: [PATCH 30/35] History.md update for forEach/map arguments. --- History.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History.md b/History.md index 6c2d8769ba..e502bdc762 100644 --- a/History.md +++ b/History.md @@ -3,6 +3,9 @@ * `restrictCreationByEmail` option in `Accounts.config` to restrict new users to emails of specific domain (eg. only users with @meteor.com emails). +* Pass an index and the cursor itself to the callbacks in `cursor.forEach` and + `cursor.map`, just like the corresponding `Array` methods. #63 + * Better error when passing a string to {{#each}}. #722 * Write dates to Mongo as ISODate rather than Integer; existing data can be From 4893fe048c556a196c146b074fc2fb45b55734df Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 13 Aug 2013 21:50:45 -0700 Subject: [PATCH 31/35] Package for security-related http headers. --- docs/client/docs.js | 4 +- docs/client/packages.html | 2 + docs/client/packages/browser-policy.html | 126 +++++++++++ .../packages/starter-browser-policy.html | 25 +++ packages/browser-policy/.gitignore | 1 + packages/browser-policy/browser-policy.js | 200 ++++++++++++++++++ packages/browser-policy/package.js | 9 + packages/starter-browser-policy/.gitignore | 1 + packages/starter-browser-policy/package.js | 8 + .../starter-browser-policy.js | 13 ++ packages/webapp/webapp_server.js | 40 +++- tools/app.html.in | 4 +- 12 files changed, 419 insertions(+), 14 deletions(-) create mode 100644 docs/client/packages/browser-policy.html create mode 100644 docs/client/packages/starter-browser-policy.html create mode 100644 packages/browser-policy/.gitignore create mode 100644 packages/browser-policy/browser-policy.js create mode 100644 packages/browser-policy/package.js create mode 100644 packages/starter-browser-policy/.gitignore create mode 100644 packages/starter-browser-policy/package.js create mode 100644 packages/starter-browser-policy/starter-browser-policy.js diff --git a/docs/client/docs.js b/docs/client/docs.js index c057ef71bd..036ed23c89 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -344,7 +344,9 @@ var toc = [ "spiderable", "stylus", "showdown", - "underscore" + "underscore", + "browser-policy", + "starter-browser-policy" ] ], "Command line", [ [ diff --git a/docs/client/packages.html b/docs/client/packages.html index 0ee701bbe8..ac344171cd 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -32,6 +32,8 @@ and removed with: {{> pkg_stylus}} {{> pkg_showdown}} {{> pkg_underscore}} +{{> pkg_browser_policy}} +{{> pkg_starter_browser_policy}} {{/better_markdown}} diff --git a/docs/client/packages/browser-policy.html b/docs/client/packages/browser-policy.html new file mode 100644 index 0000000000..b2da7d5117 --- /dev/null +++ b/docs/client/packages/browser-policy.html @@ -0,0 +1,126 @@ + diff --git a/docs/client/packages/browser-policy.html b/docs/client/packages/browser-policy.html index b2da7d5117..efc4c2c275 100644 --- a/docs/client/packages/browser-policy.html +++ b/docs/client/packages/browser-policy.html @@ -16,12 +16,13 @@ attacks. tells the browser where your app can load content from, which encourages safe practices and mitigates the damage of a cross-site-scripting attack. -For most apps, we recommend that you use the `starter-browser-policy` package to -enable reasonable policies, and then use the functions below to tighten or relax -the policies as necessary. For example, an app that only loads content from its -own origin and that doesn't use inline Javascript should use -`starter-browser-policy` and then call `BrowserPolicy.disallowInlineScripts()` -to gain additional security against cross-site scripting attacks. +For most apps, we recommend that you use the +[`starter-browser-policy`](#starterbrowserpolicy) package to enable reasonable +policies, and then use the functions below to tighten or relax the policies as +necessary. For example, an app that only loads content from its own origin and +that doesn't use inline Javascript should use `starter-browser-policy` and then +call `BrowserPolicy.disallowInlineScripts()` to gain additional security against +cross-site scripting attacks. You can use the following functions to specify which websites are allowed to frame your app: @@ -31,10 +32,11 @@ Your app will never render inside a frame or iframe. {{/dtdd}} {{#dtdd "BrowserPolicy.allowFramingByOrigin(origin)"}} -Your app will only render inside frames loaded by `origin` (such as -http://meteor.com). You can only call this function once with a single origin, -and cannot specify multiple origins that are allowed to frame your app. (This is -a limitation of the X-Frame-Options header.) +Your app will only render inside frames loaded by `origin`. You can only call +this function once with a single origin, and cannot use wildcards or specify +multiple origins that are allowed to frame your app. (This is a limitation of +the X-Frame-Options header.) Example values of `origin` include +"http://example.com" and "https://foo.example.com". {{/dtdd}} {{#dtdd "BrowserPolicy.allowFramingBySameOrigin()"}} @@ -79,17 +81,7 @@ Disallows inline CSS. Finally, you can configure a whitelist of allowed requests that various types of -content can make. Examples: - -* `BrowserPolicy.disallowObject()` causes the browser to disallow all -`` tags. -* `BrowserPolicy.allowImageOrigin("https://example.com")` -allows images to have their `src` attributes point to images served from -`https://example.com`. -* `BrowserPolicy.allowConnectOrigin("https://example.com")` allows XMLHttpRequest -and WebSocket connections to `https://example.com`. - -The following functions are defined for the content types +content can make. The following functions are defined for the content types script, object, image, media, font, and connect.
@@ -122,5 +114,16 @@ app's origin but you want to disable `` tags, you can simply call `BrowserPolicy.allowAllContentSameOrigin()` followed by `BrowserPolicy.disallowObject()`. +Other examples of using the `BrowserPolicy` API: + +* `BrowserPolicy.disallowObject()` causes the browser to disallow all +`` tags. +* `BrowserPolicy.allowImageOrigin("https://example.com")` +allows images to have their `src` attributes point to images served from +`https://example.com`. +* `BrowserPolicy.allowConnectOrigin("https://example.com")` allows XMLHttpRequest +and WebSocket connections to `https://example.com`. + + {{/better_markdown}} diff --git a/docs/client/packages/starter-browser-policy.html b/docs/client/packages/starter-browser-policy.html index bb1118fc0d..71ff88911a 100644 --- a/docs/client/packages/starter-browser-policy.html +++ b/docs/client/packages/starter-browser-policy.html @@ -2,10 +2,10 @@ {{#better_markdown}} ## `starter-browser-policy` -The `starter-browser-policy` package provides a recommended configuration for the -`browser-policy` package. When you add `starter-browser-policy` to your app, the -following policies will be enforced by browsers that support the X-Frame-Options -and Content-Security-Policy headers: +The `starter-browser-policy` package provides a recommended configuration for +the [`browser-policy`](#browserpolicy) package. When you add +`starter-browser-policy` to your app, the following policies will be enforced by +browsers that support the X-Frame-Options and Content-Security-Policy headers: * Only webpages on the same origin as your app can frame your app. * Your app can only load content (images, scripts, fonts, etc.) from its own diff --git a/packages/browser-policy/browser-policy.js b/packages/browser-policy/browser-policy.js index 3d9d8e330c..5f54c8d6d3 100644 --- a/packages/browser-policy/browser-policy.js +++ b/packages/browser-policy/browser-policy.js @@ -81,6 +81,7 @@ var removeCspSrc = function (directive, src) { }; var ensureDirective = function (directive) { + cspSrcs = cspSrcs || {}; if (! _.has(cspSrcs, directive)) cspSrcs[directive] = []; }; diff --git a/packages/webapp/package.js b/packages/webapp/package.js index 0f89553822..c451f2086a 100644 --- a/packages/webapp/package.js +++ b/packages/webapp/package.js @@ -12,6 +12,9 @@ Package.on_use(function (api) { api.use(['application-configuration'], { unordered: true }); + api.use(['browser-policy'], { + unordered: true + }); api.export(['WebApp', 'main', 'WebAppInternals'], 'server'); api.add_files('webapp_server.js', 'server'); }); From a102872a963e367962f91291a37b0fff7df1e38e Mon Sep 17 00:00:00 2001 From: Emily Stark Date: Tue, 10 Sep 2013 17:08:42 -0700 Subject: [PATCH 33/35] Rework browser-policy to make API more intuitive. - Remove starter-browser-policy and replace it with BrowserPolicy.enableContentSecurityPolicy(), which gives you the starter policy and allows you to use the other BrowserPolicy functions to configure it. This is motivated by the fact that the API isn't very intuitive without a well-defined starting policy. ex: if the package starts off without a policy, and then the user calls allowAllContentSameOrigin(), that will result in turning off inline scripts, which is probably not what they wanted. - AllContent functions do more of what you'd expect now; i.e. BrowserPolicy.disallowAllContent() actually disallows all content, instead of setting default-src to 'none', which will allow other types of content that have previously had srcs set for them. - Add some tests --- docs/client/docs.js | 1 - docs/client/packages.html | 1 - docs/client/packages/browser-policy.html | 53 +++++-- .../packages/starter-browser-policy.html | 25 --- .../browser-policy/browser-policy-test.js | 123 +++++++++++++++ packages/browser-policy/browser-policy.js | 148 +++++++++++++----- packages/browser-policy/package.js | 5 + packages/starter-browser-policy/.gitignore | 1 - packages/starter-browser-policy/package.js | 8 - .../starter-browser-policy.js | 13 -- 10 files changed, 277 insertions(+), 101 deletions(-) delete mode 100644 docs/client/packages/starter-browser-policy.html create mode 100644 packages/browser-policy/browser-policy-test.js delete mode 100644 packages/starter-browser-policy/.gitignore delete mode 100644 packages/starter-browser-policy/package.js delete mode 100644 packages/starter-browser-policy/starter-browser-policy.js diff --git a/docs/client/docs.js b/docs/client/docs.js index dc489c7cb5..3ac001e554 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -345,7 +345,6 @@ var toc = [ "spiderable", "stylus", "showdown", - "starter-browser-policy", "underscore" ] ], diff --git a/docs/client/packages.html b/docs/client/packages.html index c891bbc5cd..077e4f1cd1 100644 --- a/docs/client/packages.html +++ b/docs/client/packages.html @@ -32,7 +32,6 @@ and removed with: {{> pkg_spiderable}} {{> pkg_stylus}} {{> pkg_showdown}} -{{> pkg_starter_browser_policy}} {{> pkg_underscore}} {{/better_markdown}} diff --git a/docs/client/packages/browser-policy.html b/docs/client/packages/browser-policy.html index efc4c2c275..5b8e31354e 100644 --- a/docs/client/packages/browser-policy.html +++ b/docs/client/packages/browser-policy.html @@ -16,13 +16,26 @@ attacks. tells the browser where your app can load content from, which encourages safe practices and mitigates the damage of a cross-site-scripting attack. -For most apps, we recommend that you use the -[`starter-browser-policy`](#starterbrowserpolicy) package to enable reasonable -policies, and then use the functions below to tighten or relax the policies as -necessary. For example, an app that only loads content from its own origin and -that doesn't use inline Javascript should use `starter-browser-policy` and then -call `BrowserPolicy.disallowInlineScripts()` to gain additional security against -cross-site scripting attacks. +For most apps, we recommend that you take the following steps when using +`browser-policy`: + +* Call `BrowserPolicy.enableContentSecurityPolicy()` to enable a starter policy +for your app. With this starter policy, your app's client code will be able to +load content (images, scripts, fonts, etc.) only from its own origin, except +that XMLHttpRequests and WebSocket connections can go to any origin. Further, +your app's client code will not be able to use functions such as `eval()` that +convert strings to code. +* You can use the functions described below to customize the content +security policy. If your app does not need any inline Javascript such as inline +`