Merge branch 'security-headers' into devel

This commit is contained in:
Emily Stark
2013-09-28 18:45:23 -07:00
10 changed files with 616 additions and 13 deletions

View File

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

View File

@@ -22,6 +22,7 @@ and removed with:
{{> pkg_audit_argument_checks}}
{{> pkg_backbone}}
{{> pkg_bootstrap}}
{{> pkg_browser_policy}}
{{> pkg_coffeescript}}
{{> pkg_d3}}
{{> pkg_force_ssl}}

View File

@@ -0,0 +1,156 @@
<template name="pkg_browser_policy">
{{#better_markdown}}
## `browser-policy`
The `browser-policy` package lets you set security-related policies that will be
enforced by newer browsers. These policies help you prevent and mitigate common
attacks like cross-site scripting and clickjacking.
`browser-policy` lets you configure the HTTP headers X-Frame-Options and
Content-Security-Policy. X-Frame-Options tells the browser which websites are
allowed to frame your app. You should only let trusted websites frame your app,
because malicious sites could harm your users
with <a href="https://www.owasp.org/index.php/Clickjacking">clickjacking
attacks</a>.
<a href="https://developer.mozilla.org/en-US/docs/Security/CSP/Introducing_Content_Security_Policy">Content-Security-Policy</a>
tells the browser where your app can load content from, which encourages safe
practices and mitigates the damage of a cross-site-scripting attack.
For most apps, we recommend that you take the following steps when using
`browser-policy`:
* Call `BrowserPolicy.enableContentSecurityPolicy()` to enable a starter policy
for your app. With this starter policy, your app's client code will be able to
load content (images, scripts, fonts, etc.) only from its own origin, except
that XMLHttpRequests and WebSocket connections can go to any origin. Further,
your app's client code will not be able to use functions such as `eval()` that
convert strings to code.
* You can use the functions described below to customize the content
security policy. If your app does not need any inline Javascript such as inline
`<script>` tags, we recommend that you modify the policy by calling
`BrowserPolicy.disallowInlineScripts()` in server code. This will result in one
extra round trip when your app is loaded, but will help prevent cross-site
scripting attacks by disabling all scripts except those loaded from a `script
src` attribute.
* If your app does not need to be framed by other websites, call
`BrowserPolicy.allowFramingBySameOrigin()` to help prevent clickjacking attacks.
Meteor determines the browser policy when the server starts up, so you should
call `BrowserPolicy` functions in top-level application code or in
`Meteor.startup`.
#### Frame options
You can use the following functions to specify which websites are allowed to
frame your app:
<dl class="callbacks">
{{#dtdd "BrowserPolicy.disallowFraming()"}}
Your app will never render inside a frame or iframe.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allowFramingByOrigin(origin)"}}
Your app will only render inside frames loaded by `origin`. You can only call
this function once with a single origin, and cannot use wildcards or specify
multiple origins that are allowed to frame your app. (This is a limitation of
the X-Frame-Options header.) Example values of `origin` include
"http://example.com" and "https://foo.example.com". Note that this value of the
X-Frame-Options header is not yet supported in Chrome or Safari and will be
ignored in those browsers.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allowFramingBySameOrigin()"}}
Your app will only render inside frames loaded by webpages on the same origin as
your app.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allowFramingByAnyOrigin()"}}
Your app can be framed by any website.
{{/dtdd}}
</dl>
#### Content options
You can use the functions in this section to control how different types of
content can be loaded on your site. In order to use any of these functions, you
must first call `BrowserPolicy.enableContentSecurityPolicy()`, which enables the
starter policy described above. This section covers additional functions that
you can use to tighten or relax restrictions on what content your app can use.
You can use the following functions to adjust policies on where Javascript and
CSS can be run:
<dl class="callbacks">
{{#dtdd "BrowserPolicy.allowInlineScripts()"}}
Allows inline `<script>` tags, `javascript:` URLs, and inline event handlers.
{{/dtdd}}
{{#dtdd "BrowserPolicy.disallowInlineScripts()"}}
Disallows inline Javascript. Calling this function results in an extra
round-trip on page load to retrieve Meteor runtime configuration that is usually
part of an inline script tag.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allowEval()"}}
Allows the creation of Javascript code from strings using function such as `eval()`.
{{/dtdd}}
{{#dtdd "BrowserPolicy.disallowEval()"}}
Disallows eval and related functions.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allowInlineStyles()"}}
Allows inline style tags and style attributes.
{{/dtdd}}
{{#dtdd "BrowserPolicy.disallowInlineStyles()"}}
Disallows inline CSS.
{{/dtdd}}
</dl>
Finally, you can configure a whitelist of allowed requests that various types of
content can make. The following functions are defined for the content types
script, object, image, media, font, and connect.
<dl class="callbacks">
{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;Origin(origin)"}}
Allows this type of content to be loaded from the given origin. `origin` is a
string and can include an optional scheme (such as `http` or `https`), an
optional wildcard at the beginning, and an optional port which can be a
wildcard. Examples include `example.com`, `https://*.example.com`, and
`example.com:*`. You can call these functions multiple times with different
origins to specify a whitelist of allowed origins.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;DataUrl()"}}
Allows this type of content to be loaded from a `data:` URL.
{{/dtdd}}
{{#dtdd "BrowserPolicy.allow&lt;ContentType&gt;SameOrigin()"}}
Allows this type of content to be loaded from the same origin as your app.
{{/dtdd}}
{{#dtdd "BrowserPolicy.disallow&lt;ContentType&gt;()"}}
Disallows this type of content on your app.
{{/dtdd}}
</dl>
These functions are also defined for the content type `AllContent`, which is a
shorthand for calling one of the above functions once for each content type.
For example, if you want to allow the origin `https://foo.com` for all types of
content but you want to disable `<object>` tags, you can call
`BrowserPolicy.allowAllContentOrigin("https://foo.com")` followed by
`BrowserPolicy.disallowObject()`.
Other examples of using the `BrowserPolicy` API:
* `BrowserPolicy.disallowObject()` causes the browser to disallow all
`<object>` tags.
* `BrowserPolicy.allowImageOrigin("https://example.com")`
allows images to have their `src` attributes point to images served from
`https://example.com`.
* `BrowserPolicy.allowConnectOrigin("https://example.com")` allows XMLHttpRequest
and WebSocket connections to `https://example.com`.
{{/better_markdown}}
</template>

1
packages/browser-policy/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,123 @@
var cspsEqual = function (csp1, csp2) {
var cspToObj = function (csp) {
csp = csp.substring(0, csp.length - 1);
var parts = _.map(csp.split("; "), function (part) {
return part.split(" ");
});
var keys = _.map(parts, _.first);
var values = _.map(parts, _.rest);
_.each(values, function (value) {
value.sort();
});
return _.object(keys, values);
};
return EJSON.equals(cspToObj(csp1), cspToObj(csp2));
};
Tinytest.add("browser-policy - csp", function (test) {
var defaultCsp = "default-src 'self'; script-src 'self' 'unsafe-inline'; " +
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"
BrowserPolicy.enableContentSecurityPolicy(true /* enable for tests */);
// Default policy
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true /* tests-only flag */));
// Redundant whitelisting (inline scripts already allowed in default policy)
BrowserPolicy.allowInlineScripts();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), defaultCsp));
// Disallow inline scripts
BrowserPolicy.disallowInlineScripts();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; script-src 'self'; " +
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
// Allow eval
BrowserPolicy.allowEval();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
"connect-src * 'self'; img-src data: 'self'; style-src 'self' 'unsafe-inline';"));
// Disallow inline styles
BrowserPolicy.disallowInlineStyles();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self'; script-src 'self' 'unsafe-eval'; " +
"connect-src * 'self'; img-src data: 'self'; style-src 'self';"));
// Allow data: urls everywhere
BrowserPolicy.allowAllContentDataUrl();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self' data:; script-src 'self' 'unsafe-eval' data:; " +
"connect-src * data: 'self'; img-src data: 'self'; style-src 'self' data:;"));
// Disallow everything
BrowserPolicy.disallowAllContent();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'none';"));
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
// Put inline scripts back in
BrowserPolicy.allowInlineScripts();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'none'; script-src 'unsafe-inline';"));
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
// Add 'self' to all content types
BrowserPolicy.allowAllContentSameOrigin();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
test.isTrue(BrowserPolicy.inlineScriptsAllowed(true));
// Disallow all content except same-origin scripts
BrowserPolicy.disallowAllContent();
BrowserPolicy.allowScriptSameOrigin();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'none'; script-src 'self';"));
test.isFalse(BrowserPolicy.inlineScriptsAllowed(true));
// Starting with all content same origin, disallowScript() and then allow
// inline scripts. Result should be that that only inline scripts can execute,
// not same-origin scripts.
BrowserPolicy.disallowAllContent();
BrowserPolicy.allowAllContentSameOrigin();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(), "default-src 'self';"));
BrowserPolicy.disallowScript();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; script-src 'none';"));
BrowserPolicy.allowInlineScripts();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; script-src 'unsafe-inline';"));
// Starting with all content same origin, allow inline scripts. (Should result
// in both same origin and inline scripts allowed.)
BrowserPolicy.disallowAllContent();
BrowserPolicy.allowAllContentSameOrigin();
BrowserPolicy.allowInlineScripts();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; script-src 'self' 'unsafe-inline';"));
BrowserPolicy.disallowInlineScripts();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; script-src 'self';"));
// Allow same origin for all content, then disallow object entirely.
BrowserPolicy.disallowAllContent();
BrowserPolicy.allowAllContentSameOrigin();
BrowserPolicy.disallowObject();
test.isTrue(cspsEqual(BrowserPolicy._constructCsp(),
"default-src 'self'; object-src 'none';"));
});
Tinytest.add("browser-policy - x-frame-options", function (test) {
BrowserPolicy._reset();
BrowserPolicy.disallowFraming();
test.equal(BrowserPolicy._constructXFrameOptions(), "DENY");
BrowserPolicy.allowFramingBySameOrigin();
test.equal(BrowserPolicy._constructXFrameOptions(), "SAMEORIGIN");
BrowserPolicy.allowFramingByOrigin("foo.com");
test.equal(BrowserPolicy._constructXFrameOptions(), "ALLOW-FROM foo.com");
test.throws(function () {
BrowserPolicy.allowFramingByOrigin("bar.com");
});
BrowserPolicy.allowFramingByAnyOrigin();
test.isFalse(BrowserPolicy._constructXFrameOptions());
});

View File

@@ -0,0 +1,284 @@
// To enable CSP, call BrowserPolicy.enableContentSecurityPolicy(). This enables
// the following default policy:
// No eval or other string-to-code, and content can only be loaded from the
// same origin as the app (except for XHRs and websocket connections, which can
// go to any origin).
//
// Apps should call BrowserPolicy.allowFramingBySameOrigin() to allow only
// same-origin pages to frame their apps, if they don't explicitly want to be
// framed by third-party sites.
//
// Apps should call BrowserPolicy.disallowInlineScripts() if they are not using
// any inline script tags and are willing to accept an extra round trip on page
// load.
//
// BrowserPolicy functions for tweaking CSP:
// allowInlineScripts()
// disallowInlineScripts(): adds extra round-trip to page load time
// allowInlineStyles()
// disallowInlineStyles()
// allowEval()
// disallowEval()
//
// For each type of content (script, object, image, media, font, connect,
// style), there are the following functions:
// allow<content type>Origin(origin): allows the type of content to be loaded
// from the given origin
// allow<content type>DataUrl(): allows the content to be loaded from data: URLs
// allow<content type>SameOrigin(): allows the content to be loaded from the
// same origin
// disallow<content type>(): disallows this type of content all together (can't
// be called for script)
//
// The following functions allow you to set rules for all types of content at
// once:
// allowAllContentOrigin(origin)
// allowAllContentDataUrl()
// allowAllContentSameOrigin()
// disallowAllContent()
//
//
// For controlling which origins can frame this app,
// BrowserPolicy.disallowFraming()
// BrowserPolicy.allowFramingByOrigin(origin)
// BrowserPolicy.allowFramingBySameOrigin()
// BrowserPolicy.allowFramingByAnyOrigin();
var xFrameOptions;
var cspSrcs;
// CSP keywords have to be single-quoted.
var unsafeInline = "'unsafe-inline'";
var unsafeEval = "'unsafe-eval'";
var selfKeyword = "'self'";
var noneKeyword = "'none'";
var cspEnabled = false;
var cspEnabledForTests = false;
BrowserPolicy = {};
// Exported for tests.
var constructXFrameOptions = BrowserPolicy._constructXFrameOptions =
function () {
return xFrameOptions;
};
var constructCsp = BrowserPolicy._constructCsp = function () {
cspSrcs = cspSrcs || {};
var header = _.map(cspSrcs, function (srcs, directive) {
srcs = srcs || [];
if (_.isEmpty(srcs))
srcs = [noneKeyword];
var directiveCsp = _.uniq(srcs).join(" ");
return directive + " " + directiveCsp + ";";
});
header = header.join(" ");
return header;
};
var parseCsp = function (csp) {
var policies = csp.split("; ");
cspSrcs = {};
_.each(policies, function (policy) {
if (policy[policy.length - 1] === ";")
policy = policy.substring(0, policy.length - 1);
var srcs = policy.split(" ");
var directive = srcs[0];
if (_.indexOf(srcs, noneKeyword) !== -1)
cspSrcs[directive] = null;
else
cspSrcs[directive] = srcs.slice(1);
});
if (cspSrcs["default-src"] === undefined)
throw new Error("Content Security Policies used with " +
"browser-policy must specify a default-src.");
// Copy default-src sources to other directives.
_.each(cspSrcs, function (sources, directive) {
cspSrcs[directive] = _.union(sources || [], cspSrcs["default-src"] || []);
});
};
var removeCspSrc = function (directive, src) {
cspSrcs[directive] = _.without(cspSrcs[directive] || [], src);
};
var ensureDirective = function (directive) {
throwIfNotEnabled();
cspSrcs = cspSrcs || {};
if (! _.has(cspSrcs, directive))
cspSrcs[directive] = _.clone(cspSrcs["default-src"]);
};
var throwIfNotEnabled = function () {
if (! cspEnabled && ! cspEnabledForTests)
throw new Error("Enable this function by calling "+
"BrowserPolicy.enableContentSecurityPolicy().");
};
WebApp.connectHandlers.use(function (req, res, next) {
if (xFrameOptions)
res.setHeader("X-Frame-Options", constructXFrameOptions());
if (cspEnabled)
res.setHeader("Content-Security-Policy", constructCsp());
next();
});
BrowserPolicy = _.extend(BrowserPolicy, {
_reset: function () {
xFrameOptions = null;
cspSrcs = null;
cspEnabled = false;
},
allowFramingBySameOrigin: function () {
xFrameOptions = "SAMEORIGIN";
},
disallowFraming: function () {
xFrameOptions = "DENY";
},
// ALLOW-FROM not supported in Chrome or Safari.
allowFramingByOrigin: function (origin) {
// Trying to specify two allow-from throws to prevent users from
// accidentally overwriting an allow-from origin when they think they are
// adding multiple origins.
if (xFrameOptions && xFrameOptions.indexOf("ALLOW-FROM") === 0)
throw new Error("You can only specify one origin that is allowed to" +
" frame this app.");
xFrameOptions = "ALLOW-FROM " + origin;
},
allowFramingByAnyOrigin: function () {
xFrameOptions = null;
},
// _enableForTests means that you can call CSP functions, but the header won't
// actually be sent.
enableContentSecurityPolicy: function (_enableForTests) {
// By default, unsafe inline scripts and styles are allowed, since we expect
// many apps will use them for analytics, etc. Unsafe eval is disallowed, and
// the only allowable content source is the same origin or data, except for
// connect which allows anything (since meteor.com apps make websocket
// connections to a lot of different origins).
if (! _enableForTests)
cspEnabled = true;
else
cspEnabledForTests = true;
cspSrcs = {};
BrowserPolicy.setContentSecurityPolicy("default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"connect-src *; " +
"img-src data: 'self'; " +
"style-src 'self' 'unsafe-inline';");
},
setContentSecurityPolicy: function (csp) {
throwIfNotEnabled();
parseCsp(csp);
},
// Helpers for creating content security policies
_keywordAllowed: function (directive, keyword, _calledFromTests) {
// All keywords are allowed if csp is not enabled and we're not in a test
// run. If csp is enabled or we're in a test run, then look in cspSrcs to
// see if it's allowed.
return (! cspEnabled && ! _calledFromTests) ||
(cspSrcs[directive] &&
_.indexOf(cspSrcs[directive], keyword) !== -1);
},
// Used by webapp to determine whether we need an extra round trip for
// __meteor_runtime_config__.
// _calledFromTests is used to indicate that we should ignore cspEnabled and
// instead look directly in cspSrcs to determine if the keyword is allowed.
// XXX maybe this test interface could be cleaned up
inlineScriptsAllowed: function (_calledFromTests) {
return BrowserPolicy._keywordAllowed("script-src",
unsafeInline, _calledFromTests);
},
allowInlineScripts: function () {
ensureDirective("script-src");
cspSrcs["script-src"].push(unsafeInline);
},
disallowInlineScripts: function () {
ensureDirective("script-src");
removeCspSrc("script-src", unsafeInline);
},
allowEval: function () {
ensureDirective("script-src");
cspSrcs["script-src"].push(unsafeEval);
},
disallowEval: function () {
ensureDirective("script-src");
removeCspSrc("script-src", unsafeEval);
},
allowInlineStyles: function () {
ensureDirective("style-src");
cspSrcs["style-src"].push(unsafeInline);
},
disallowInlineStyles: function () {
ensureDirective("style-src");
removeCspSrc("style-src", unsafeInline);
},
// Functions for setting defaults
allowAllContentSameOrigin: function () {
BrowserPolicy.allowAllContentOrigin(selfKeyword);
},
allowAllContentDataUrl: function () {
BrowserPolicy.allowAllContentOrigin("data:");
},
allowAllContentOrigin: function (origin) {
ensureDirective("default-src");
_.each(_.keys(cspSrcs), function (directive) {
cspSrcs[directive].push(origin);
});
},
disallowAllContent: function () {
throwIfNotEnabled();
cspSrcs = {
"default-src": []
};
}
});
// allow<Resource>Origin, allow<Resource>Data, allow<Resource>self, and
// disallow<Resource> methods for each type of resource.
_.each(["script", "object", "img", "media",
"font", "connect", "style"],
function (resource) {
var directive = resource + "-src";
var methodResource;
if (resource !== "img") {
methodResource = resource.charAt(0).toUpperCase() +
resource.slice(1);
} else {
methodResource = "Image";
}
var allowMethodName = "allow" + methodResource + "Origin";
var disallowMethodName = "disallow" + methodResource;
var allowDataMethodName = "allow" + methodResource + "DataUrl";
var allowSelfMethodName = "allow" + methodResource + "SameOrigin";
BrowserPolicy[allowMethodName] = function (src) {
ensureDirective(directive);
cspSrcs[directive].push(src);
};
BrowserPolicy[disallowMethodName] = function () {
throwIfNotEnabled();
cspSrcs[directive] = [];
};
BrowserPolicy[allowDataMethodName] = function () {
ensureDirective(directive);
cspSrcs[directive].push("data:");
};
BrowserPolicy[allowSelfMethodName] = function () {
ensureDirective(directive);
cspSrcs[directive].push(selfKeyword);
};
});

View File

@@ -0,0 +1,14 @@
Package.describe({
summary: "Configure security policies enforced by the browser"
});
Package.on_use(function (api) {
api.use(["underscore", "webapp"], "server");
api.add_files("browser-policy.js", "server");
api.export("BrowserPolicy", "server");
});
Package.on_test(function (api) {
api.use(["tinytest", "browser-policy", "ejson"]);
api.add_files("browser-policy-test.js", "server");
});

View File

@@ -12,6 +12,11 @@ Package.on_use(function (api) {
api.use(['application-configuration'], {
unordered: true
});
// At response serving time, webapp uses browser-policy if it is loaded. If
// browser-policy is loaded, then it must be loaded after webapp
// (browser-policy depends on webapp). So we don't explicitly depend in any
// way on browser-policy here, but we use it when it is loaded, and it can be
// loaded after webapp.
api.export(['WebApp', 'main', 'WebAppInternals'], 'server');
api.add_files('webapp_server.js', 'server');
});

View File

@@ -147,7 +147,6 @@ var appUrl = function (url) {
return true;
};
var runWebAppServer = function () {
// read the control for the client we'll be serving up
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir,
@@ -231,6 +230,17 @@ var runWebAppServer = function () {
next();
return;
}
if (Package["browser-policy"] &&
! Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed() &&
pathname === "/meteor_runtime_config.js") {
res.writeHead(200, { 'Content-type': 'application/javascript' });
res.write("__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";");
res.end();
return;
}
if (!_.has(staticFiles, pathname)) {
next();
return;
@@ -387,15 +397,25 @@ var runWebAppServer = function () {
argv = optimist(argv).boolean('keepalive').argv;
var boilerplateHtmlPath = path.join(clientDir, clientJson.page);
boilerplateHtml =
fs.readFileSync(boilerplateHtmlPath, 'utf8')
.replace(
"// ##RUNTIME_CONFIG##",
"__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";")
.replace(
/##ROOT_URL_PATH_PREFIX##/g,
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
boilerplateHtml = fs.readFileSync(boilerplateHtmlPath, 'utf8');
// Include __meteor_runtime_config__ in the app html, as an inline script if
// it's not forbidden by CSP.
if (! Package["browser-policy"] ||
Package["browser-policy"].BrowserPolicy.inlineScriptsAllowed()) {
boilerplateHtml = boilerplateHtml.replace(
/##RUNTIME_CONFIG##/,
"<script type='text/javascript'>__meteor_runtime_config__ = " +
JSON.stringify(__meteor_runtime_config__) + ";</script>");
} else {
boilerplateHtml = boilerplateHtml.replace(
/##RUNTIME_CONFIG##/,
"<script type='text/javascript' src='##ROOT_URL_PATH_PREFIX##/meteor_runtime_config.js'></script>"
);
}
boilerplateHtml = boilerplateHtml.replace(
/##ROOT_URL_PATH_PREFIX##/g,
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || "");
// only start listening after all the startup code has run.
var localPort = parseInt(process.env.PORT) || 0;

View File

@@ -4,9 +4,7 @@
{{#each stylesheets}} <link rel="stylesheet" href="##ROOT_URL_PATH_PREFIX##{{this}}">
{{/each}}
<script type="text/javascript">
// ##RUNTIME_CONFIG##
</script>
##RUNTIME_CONFIG##
{{#each scripts}} <script type="text/javascript" src="##ROOT_URL_PATH_PREFIX##{{this}}"></script>
{{/each}}