diff --git a/app/server/server.js b/app/server/server.js index 2c3a5330d5..45046d4ade 100644 --- a/app/server/server.js +++ b/app/server/server.js @@ -67,6 +67,29 @@ var runtime_config = function (app_html) { return app_html; }; +// Serve app HTML for this URL? +var appUrl = function (url) { + if (url === '/favicon.ico' || url === '/robots.txt') + return false; + + // NOTE: app.manifest is not a web standard like favicon.ico and + // robots.txt. It is a file name we have chosen to use for HTML5 + // appcache URLs. It is included here to prevent using an appcache + // then removing it from poisoning an app permanently. Eventually, + // once we have server side routing, this won't be needed as + // unknown URLs with return a 404 automatically. + if (url === '/app.manifest') + return false; + + // Avoid serving app HTML for declared network routes such as /sockjs/. + if (__meteor_bootstrap__._routePolicy && + __meteor_bootstrap__._routePolicy.classify(url) === 'network') + return false; + + // we currently return app HTML on all URLs by default + return true; +} + var run = function () { var bundle_dir = path.join(__dirname, '..'); @@ -131,21 +154,9 @@ var run = function () { app_html = runtime_config(app_html); - app.use(function (req, res) { - // prevent these URLs from returning app_html - // - // NOTE: app.manifest is not a web standard like favicon.ico and - // robots.txt. It is a file name we have chosen to use for HTML5 - // appcache URLs. It is included here to prevent using an appcache - // then removing it from poisoning an app permanently. Eventually, - // once we have server side routing, this won't be needed as - // unknown URLs with return a 404 automatically. - if (_.indexOf(['/app.manifest', '/favicon.ico', '/robots.txt'], req.url) - !== -1) { - res.writeHead(404); - res.end(); - return; - } + app.use(function (req, res, next) { + if (! appUrl(req.url)) + return next(); res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'}); if (supported_browser(req.headers['user-agent'])) @@ -155,6 +166,13 @@ var run = function () { res.end(); }); + // Return 404 by default, if no other handlers serve this URL. + app.use(function (req, res) { + res.writeHead(404); + res.end(); + return; + }); + // run the user startup hooks. _.each(__meteor_bootstrap__.startup_hooks, function (x) { x(); }); diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index 7feea25e99..64bc8224eb 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -1,6 +1,8 @@ (function () { var connect = __meteor_bootstrap__.require("connect"); + Meteor._routePolicy.declare('/_oauth/', 'network'); + Accounts.oauth._services = {}; // Register a handler for an OAuth service. The handler will be called diff --git a/packages/accounts-oauth-helper/package.js b/packages/accounts-oauth-helper/package.js index 9fe117d37f..76a8a9b43a 100644 --- a/packages/accounts-oauth-helper/package.js +++ b/packages/accounts-oauth-helper/package.js @@ -5,6 +5,7 @@ Package.describe({ Package.on_use(function (api) { api.use('accounts-base', ['client', 'server']); + api.use('routepolicy', 'server'); api.add_files('oauth_common.js', ['client', 'server']); api.add_files('oauth_client.js', 'client'); diff --git a/packages/bootstrap/css/bootstrap-override.css b/packages/bootstrap/css/bootstrap-override.css index 686584e6ea..169165e46a 100644 --- a/packages/bootstrap/css/bootstrap-override.css +++ b/packages/bootstrap/css/bootstrap-override.css @@ -1,4 +1,4 @@ -/* +/* * XXX Hack to make bootstrap work when bundled. This needs to be included * _after_ the standard bootstrap css files. * @@ -10,7 +10,28 @@ [class*=" icon-"] { background-image: url("/packages/bootstrap/img/glyphicons-halflings.png"); } - -.icon-white { +/* + * Selectors borrowed from bootstrap.css. For all releases of bootstrap, when + * we upgrade, update this file to borrow the selectors from where bootstrap.css + * references any .png. When we update to using .less instead, use the less + * directives in a less file instead. + */ +.icon-white, +.nav-pills > .active > a > [class^="icon-"], +.nav-pills > .active > a > [class*=" icon-"], +.nav-list > .active > a > [class^="icon-"], +.nav-list > .active > a > [class*=" icon-"], +.navbar-inverse .nav > .active > a > [class^="icon-"], +.navbar-inverse .nav > .active > a > [class*=" icon-"], +.dropdown-menu > li > a:hover > [class^="icon-"], +.dropdown-menu > li > a:focus > [class^="icon-"], +.dropdown-menu > li > a:hover > [class*=" icon-"], +.dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > .active > a > [class^="icon-"], +.dropdown-menu > .active > a > [class*=" icon-"], +.dropdown-submenu:hover > a > [class^="icon-"], +.dropdown-submenu:focus > a > [class^="icon-"], +.dropdown-submenu:hover > a > [class*=" icon-"], +.dropdown-submenu:focus > a > [class*=" icon-"] { background-image: url("/packages/bootstrap/img/glyphicons-halflings-white.png"); } diff --git a/packages/minimongo/minimongo_tests.js b/packages/minimongo/minimongo_tests.js index 33edc01829..8a7f973ab3 100644 --- a/packages/minimongo/minimongo_tests.js +++ b/packages/minimongo/minimongo_tests.js @@ -516,6 +516,10 @@ Tinytest.add("minimongo - selector_compiler", function (test) { nomatch({a: {$regex: 'a'}}, {a: 'cut'}); nomatch({a: {$regex: 'a'}}, {a: 'CAT'}); match({a: {$regex: 'a', $options: 'i'}}, {a: 'CAT'}); + nomatch({a: /undefined/}, {}); + nomatch({a: {$regex: 'undefined'}}, {}); + nomatch({a: /xxx/}, {}); + nomatch({a: {$regex: 'xxx'}}, {}); match({a: {$options: 'i'}}, {a: 12}); match({b: {$options: 'i'}}, {a: 12}); diff --git a/packages/minimongo/selector.js b/packages/minimongo/selector.js index 04f5c18cc0..1d8c5b5f46 100644 --- a/packages/minimongo/selector.js +++ b/packages/minimongo/selector.js @@ -45,6 +45,8 @@ var compileValueSelector = function (valueSelector) { if (valueSelector instanceof RegExp) { return function (value) { + if (value === undefined) + return false; return _anyIfArray(value, function (x) { return valueSelector.test(x); }); @@ -267,6 +269,8 @@ var VALUE_OPERATORS = { } return function (value) { + if (value === undefined) + return false; return _anyIfArray(value, function (x) { return operand.test(x); }); diff --git a/packages/routepolicy/package.js b/packages/routepolicy/package.js new file mode 100644 index 0000000000..526aa130df --- /dev/null +++ b/packages/routepolicy/package.js @@ -0,0 +1,12 @@ +Package.describe({ + summary: "route policy declarations", + internal: true +}); + +Package.on_use(function (api) { + api.add_files('routepolicy.js', 'server'); +}); + +Package.on_test(function (api) { + api.add_files(['routepolicy_tests.js'], 'server'); +}); diff --git a/packages/routepolicy/routepolicy.js b/packages/routepolicy/routepolicy.js new file mode 100644 index 0000000000..ce27701568 --- /dev/null +++ b/packages/routepolicy/routepolicy.js @@ -0,0 +1,89 @@ +(function () { + + // The route policy is a singleton in a running application, but we + // can't unit test the real singleton because messing with the real + // routes would break tinytest... so allow policy instances to be + // constructed for testing. + + Meteor.__RoutePolicyConstructor = function () { + var self = this; + self.urlPrefixTypes = {}; + }; + + _.extend(Meteor.__RoutePolicyConstructor.prototype, { + + urlPrefixMatches: function (urlPrefix, url) { + return url.substr(0, urlPrefix.length) === urlPrefix; + }, + + checkType: function (type) { + if (! _.contains(['network'], type)) + return 'the route type must be "network"'; + return null; + }, + + checkUrlPrefix: function (urlPrefix) { + var self = this; + if (urlPrefix.charAt(0) !== '/') + return 'a route URL prefix must begin with a slash'; + if (urlPrefix === '/') + return 'a route URL prefix cannot be /'; + if (self.urlPrefixTypes[urlPrefix] && self.urlPrefixTypes[urlPrefix] !== type) + return 'the route URL prefix ' + urlPrefix + ' has already been declared to be of type ' + type; + return null; + }, + + checkForConflictWithStatic: function (urlPrefix, type, _testManifest) { + var self = this; + var manifest = _testManifest || __meteor_bootstrap__.bundle.manifest; + var conflict = _.find(manifest, function (resource) { + return (resource.type === 'static' && + resource.where === 'client' && + self.urlPrefixMatches(urlPrefix, resource.url)); + }); + if (conflict) + return ('static resource ' + conflict.url + ' conflicts with ' + + type + ' route ' + urlPrefix); + else + return null; + }, + + declare: function (urlPrefix, type) { + var self = this; + var problem = self.checkType(type) || + self.checkUrlPrefix(urlPrefix) || + self.checkForConflictWithStatic(urlPrefix, type); + if (problem) + throw new Error(problem); + // TODO overlapping prefixes, e.g. /foo/ and /foo/bar/ + self.urlPrefixTypes[urlPrefix] = type; + }, + + classify: function (url) { + var self = this; + if (url.charAt(0) !== '/') + throw new Error('url must be a relative URL: ' + url); + var prefix = _.find(_.keys(self.urlPrefixTypes), function (_prefix) { + return self.urlPrefixMatches(_prefix, url); + }); + if (prefix) + return self.urlPrefixTypes[prefix]; + else + return null; + }, + + urlPrefixesFor: function (type) { + var self = this; + var prefixes = []; + _.each(self.urlPrefixTypes, function (_type, _prefix) { + if (_type === type) + prefixes.push(_prefix); + }); + return prefixes.sort(); + } + }); + + __meteor_bootstrap__._routePolicy = Meteor._routePolicy = + new Meteor.__RoutePolicyConstructor(); + +})(); diff --git a/packages/routepolicy/routepolicy_tests.js b/packages/routepolicy/routepolicy_tests.js new file mode 100644 index 0000000000..51c38eaf59 --- /dev/null +++ b/packages/routepolicy/routepolicy_tests.js @@ -0,0 +1,39 @@ +Tinytest.add("routepolicy", function (test) { + var policy = new Meteor.__RoutePolicyConstructor(); + + policy.declare('/sockjs/', 'network'); + // App routes might look like this... + // policy.declare('/posts/', 'app'); + // policy.declare('/about', 'app'); + + test.equal(policy.classify('/'), null); + test.equal(policy.classify('/foo'), null); + test.equal(policy.classify('/sockjs'), null); + + test.equal(policy.classify('/sockjs/'), 'network'); + test.equal(policy.classify('/sockjs/foo'), 'network'); + + // test.equal(policy.classify('/posts/'), 'app'); + // test.equal(policy.classify('/posts/1234'), 'app'); + + test.equal(policy.urlPrefixesFor('network'), ['/sockjs/']); + // test.equal(policy.urlPrefixesFor('app'), ['/about', '/posts/']); +}); + +Tinytest.add("routepolicy - static conflicts", function (test) { + var manifest = [ + { + "path": "static/sockjs/socks-are-comfy.jpg", + "type": "static", + "where": "client", + "cacheable": false, + "url": "/sockjs/socks-are-comfy.jpg" + }, + ]; + var policy = new Meteor.__RoutePolicyConstructor(); + + test.equal( + policy.checkForConflictWithStatic('/sockjs/', 'network', manifest), + "static resource /sockjs/socks-are-comfy.jpg conflicts with network route /sockjs/" + ); +}); diff --git a/packages/stream/package.js b/packages/stream/package.js index 340333563e..a10ab4d89c 100644 --- a/packages/stream/package.js +++ b/packages/stream/package.js @@ -6,6 +6,7 @@ Package.describe({ Package.on_use(function (api) { api.use(['underscore', 'logging', 'random', 'json'], ['client', 'server']); api.use('reload', 'client'); + api.use('routepolicy', 'server'); api.add_files('sockjs-0.3.4.js', 'client'); diff --git a/packages/stream/stream_server.js b/packages/stream/stream_server.js index 57f43c52d3..0d1a99dc30 100644 --- a/packages/stream/stream_server.js +++ b/packages/stream/stream_server.js @@ -1,3 +1,5 @@ +Meteor._routePolicy.declare('/sockjs/', 'network'); + // unique id for this instantiation of the server. If this changes // between client reconnects, the client will reload. You can set the // environment variable "SERVER_ID" to control this. For example, if