832 lines
72 KiB
JavaScript
832 lines
72 KiB
JavaScript
(function () {
|
|
|
|
/* Imports */
|
|
var Meteor = Package.meteor.Meteor;
|
|
var Log = Package.logging.Log;
|
|
var _ = Package.underscore._;
|
|
var RoutePolicy = Package.routepolicy.RoutePolicy;
|
|
var Boilerplate = Package['boilerplate-generator'].Boilerplate;
|
|
var WebAppHashing = Package['webapp-hashing'].WebAppHashing;
|
|
|
|
/* Package-scope variables */
|
|
var WebApp, main, WebAppInternals;
|
|
|
|
(function () {
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
// //
|
|
// packages/webapp/webapp_server.js //
|
|
// //
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
////////// Requires ////////// // 1
|
|
// 2
|
|
var fs = Npm.require("fs"); // 3
|
|
var http = Npm.require("http"); // 4
|
|
var os = Npm.require("os"); // 5
|
|
var path = Npm.require("path"); // 6
|
|
var url = Npm.require("url"); // 7
|
|
var crypto = Npm.require("crypto"); // 8
|
|
// 9
|
|
var connect = Npm.require('connect'); // 10
|
|
var useragent = Npm.require('useragent'); // 11
|
|
var send = Npm.require('send'); // 12
|
|
// 13
|
|
var Future = Npm.require('fibers/future'); // 14
|
|
var Fiber = Npm.require('fibers'); // 15
|
|
// 16
|
|
var SHORT_SOCKET_TIMEOUT = 5*1000; // 17
|
|
var LONG_SOCKET_TIMEOUT = 120*1000; // 18
|
|
// 19
|
|
WebApp = {}; // 20
|
|
WebAppInternals = {}; // 21
|
|
// 22
|
|
WebAppInternals.NpmModules = { // 23
|
|
connect: { // 24
|
|
version: Npm.require('connect/package.json').version, // 25
|
|
module: connect // 26
|
|
} // 27
|
|
}; // 28
|
|
// 29
|
|
WebApp.defaultArch = 'web.browser'; // 30
|
|
// 31
|
|
// XXX maps archs to manifests // 32
|
|
WebApp.clientPrograms = {}; // 33
|
|
// 34
|
|
// XXX maps archs to program path on filesystem // 35
|
|
var archPath = {}; // 36
|
|
// 37
|
|
var bundledJsCssPrefix; // 38
|
|
// 39
|
|
var sha1 = function (contents) { // 40
|
|
var hash = crypto.createHash('sha1'); // 41
|
|
hash.update(contents); // 42
|
|
return hash.digest('hex'); // 43
|
|
}; // 44
|
|
// 45
|
|
var readUtf8FileSync = function (filename) { // 46
|
|
return Meteor.wrapAsync(fs.readFile)(filename, 'utf8'); // 47
|
|
}; // 48
|
|
// 49
|
|
// #BrowserIdentification // 50
|
|
// // 51
|
|
// We have multiple places that want to identify the browser: the // 52
|
|
// unsupported browser page, the appcache package, and, eventually // 53
|
|
// delivering browser polyfills only as needed. // 54
|
|
// // 55
|
|
// To avoid detecting the browser in multiple places ad-hoc, we create a // 56
|
|
// Meteor "browser" object. It uses but does not expose the npm // 57
|
|
// useragent module (we could choose a different mechanism to identify // 58
|
|
// the browser in the future if we wanted to). The browser object // 59
|
|
// contains // 60
|
|
// // 61
|
|
// * `name`: the name of the browser in camel case // 62
|
|
// * `major`, `minor`, `patch`: integers describing the browser version // 63
|
|
// // 64
|
|
// Also here is an early version of a Meteor `request` object, intended // 65
|
|
// to be a high-level description of the request without exposing // 66
|
|
// details of connect's low-level `req`. Currently it contains: // 67
|
|
// // 68
|
|
// * `browser`: browser identification object described above // 69
|
|
// * `url`: parsed url, including parsed query params // 70
|
|
// // 71
|
|
// As a temporary hack there is a `categorizeRequest` function on WebApp which // 72
|
|
// converts a connect `req` to a Meteor `request`. This can go away once smart // 73
|
|
// packages such as appcache are being passed a `request` object directly when // 74
|
|
// they serve content. // 75
|
|
// // 76
|
|
// This allows `request` to be used uniformly: it is passed to the html // 77
|
|
// attributes hook, and the appcache package can use it when deciding // 78
|
|
// whether to generate a 404 for the manifest. // 79
|
|
// // 80
|
|
// Real routing / server side rendering will probably refactor this // 81
|
|
// heavily. // 82
|
|
// 83
|
|
// 84
|
|
// e.g. "Mobile Safari" => "mobileSafari" // 85
|
|
var camelCase = function (name) { // 86
|
|
var parts = name.split(' '); // 87
|
|
parts[0] = parts[0].toLowerCase(); // 88
|
|
for (var i = 1; i < parts.length; ++i) { // 89
|
|
parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1); // 90
|
|
} // 91
|
|
return parts.join(''); // 92
|
|
}; // 93
|
|
// 94
|
|
var identifyBrowser = function (userAgentString) { // 95
|
|
var userAgent = useragent.lookup(userAgentString); // 96
|
|
return { // 97
|
|
name: camelCase(userAgent.family), // 98
|
|
major: +userAgent.major, // 99
|
|
minor: +userAgent.minor, // 100
|
|
patch: +userAgent.patch // 101
|
|
}; // 102
|
|
}; // 103
|
|
// 104
|
|
// XXX Refactor as part of implementing real routing. // 105
|
|
WebAppInternals.identifyBrowser = identifyBrowser; // 106
|
|
// 107
|
|
WebApp.categorizeRequest = function (req) { // 108
|
|
return { // 109
|
|
browser: identifyBrowser(req.headers['user-agent']), // 110
|
|
url: url.parse(req.url, true) // 111
|
|
}; // 112
|
|
}; // 113
|
|
// 114
|
|
// HTML attribute hooks: functions to be called to determine any attributes to // 115
|
|
// be added to the '<html>' tag. Each function is passed a 'request' object (see // 116
|
|
// #BrowserIdentification) and should return null or object. // 117
|
|
var htmlAttributeHooks = []; // 118
|
|
var getHtmlAttributes = function (request) { // 119
|
|
var combinedAttributes = {}; // 120
|
|
_.each(htmlAttributeHooks || [], function (hook) { // 121
|
|
var attributes = hook(request); // 122
|
|
if (attributes === null) // 123
|
|
return; // 124
|
|
if (typeof attributes !== 'object') // 125
|
|
throw Error("HTML attribute hook must return null or object"); // 126
|
|
_.extend(combinedAttributes, attributes); // 127
|
|
}); // 128
|
|
return combinedAttributes; // 129
|
|
}; // 130
|
|
WebApp.addHtmlAttributeHook = function (hook) { // 131
|
|
htmlAttributeHooks.push(hook); // 132
|
|
}; // 133
|
|
// 134
|
|
// Serve app HTML for this URL? // 135
|
|
var appUrl = function (url) { // 136
|
|
if (url === '/favicon.ico' || url === '/robots.txt') // 137
|
|
return false; // 138
|
|
// 139
|
|
// NOTE: app.manifest is not a web standard like favicon.ico and // 140
|
|
// robots.txt. It is a file name we have chosen to use for HTML5 // 141
|
|
// appcache URLs. It is included here to prevent using an appcache // 142
|
|
// then removing it from poisoning an app permanently. Eventually, // 143
|
|
// once we have server side routing, this won't be needed as // 144
|
|
// unknown URLs with return a 404 automatically. // 145
|
|
if (url === '/app.manifest') // 146
|
|
return false; // 147
|
|
// 148
|
|
// Avoid serving app HTML for declared routes such as /sockjs/. // 149
|
|
if (RoutePolicy.classify(url)) // 150
|
|
return false; // 151
|
|
// 152
|
|
// we currently return app HTML on all URLs by default // 153
|
|
return true; // 154
|
|
}; // 155
|
|
// 156
|
|
// 157
|
|
// We need to calculate the client hash after all packages have loaded // 158
|
|
// to give them a chance to populate __meteor_runtime_config__. // 159
|
|
// // 160
|
|
// Calculating the hash during startup means that packages can only // 161
|
|
// populate __meteor_runtime_config__ during load, not during startup. // 162
|
|
// // 163
|
|
// Calculating instead it at the beginning of main after all startup // 164
|
|
// hooks had run would allow packages to also populate // 165
|
|
// __meteor_runtime_config__ during startup, but that's too late for // 166
|
|
// autoupdate because it needs to have the client hash at startup to // 167
|
|
// insert the auto update version itself into // 168
|
|
// __meteor_runtime_config__ to get it to the client. // 169
|
|
// // 170
|
|
// An alternative would be to give autoupdate a "post-start, // 171
|
|
// pre-listen" hook to allow it to insert the auto update version at // 172
|
|
// the right moment. // 173
|
|
// 174
|
|
Meteor.startup(function () { // 175
|
|
var calculateClientHash = WebAppHashing.calculateClientHash; // 176
|
|
WebApp.clientHash = function (archName) { // 177
|
|
archName = archName || WebApp.defaultArch; // 178
|
|
return calculateClientHash(WebApp.clientPrograms[archName].manifest); // 179
|
|
}; // 180
|
|
// 181
|
|
WebApp.calculateClientHashRefreshable = function (archName) { // 182
|
|
archName = archName || WebApp.defaultArch; // 183
|
|
return calculateClientHash(WebApp.clientPrograms[archName].manifest, // 184
|
|
function (name) { // 185
|
|
return name === "css"; // 186
|
|
}); // 187
|
|
}; // 188
|
|
WebApp.calculateClientHashNonRefreshable = function (archName) { // 189
|
|
archName = archName || WebApp.defaultArch; // 190
|
|
return calculateClientHash(WebApp.clientPrograms[archName].manifest, // 191
|
|
function (name) { // 192
|
|
return name !== "css"; // 193
|
|
}); // 194
|
|
}; // 195
|
|
WebApp.calculateClientHashCordova = function () { // 196
|
|
var archName = 'web.cordova'; // 197
|
|
if (! WebApp.clientPrograms[archName]) // 198
|
|
return 'none'; // 199
|
|
// 200
|
|
return calculateClientHash( // 201
|
|
WebApp.clientPrograms[archName].manifest, null, _.pick( // 202
|
|
__meteor_runtime_config__, 'PUBLIC_SETTINGS')); // 203
|
|
}; // 204
|
|
}); // 205
|
|
// 206
|
|
// 207
|
|
// 208
|
|
// When we have a request pending, we want the socket timeout to be long, to // 209
|
|
// give ourselves a while to serve it, and to allow sockjs long polls to // 210
|
|
// complete. On the other hand, we want to close idle sockets relatively // 211
|
|
// quickly, so that we can shut down relatively promptly but cleanly, without // 212
|
|
// cutting off anyone's response. // 213
|
|
WebApp._timeoutAdjustmentRequestCallback = function (req, res) { // 214
|
|
// this is really just req.socket.setTimeout(LONG_SOCKET_TIMEOUT); // 215
|
|
req.setTimeout(LONG_SOCKET_TIMEOUT); // 216
|
|
// Insert our new finish listener to run BEFORE the existing one which removes // 217
|
|
// the response from the socket. // 218
|
|
var finishListeners = res.listeners('finish'); // 219
|
|
// XXX Apparently in Node 0.12 this event is now called 'prefinish'. // 220
|
|
// https://github.com/joyent/node/commit/7c9b6070 // 221
|
|
res.removeAllListeners('finish'); // 222
|
|
res.on('finish', function () { // 223
|
|
res.setTimeout(SHORT_SOCKET_TIMEOUT); // 224
|
|
}); // 225
|
|
_.each(finishListeners, function (l) { res.on('finish', l); }); // 226
|
|
}; // 227
|
|
// 228
|
|
// 229
|
|
// Will be updated by main before we listen. // 230
|
|
// Map from client arch to boilerplate object. // 231
|
|
// Boilerplate object has: // 232
|
|
// - func: XXX // 233
|
|
// - baseData: XXX // 234
|
|
var boilerplateByArch = {}; // 235
|
|
// 236
|
|
// Given a request (as returned from `categorizeRequest`), return the // 237
|
|
// boilerplate HTML to serve for that request. Memoizes on HTML // 238
|
|
// attributes (used by, eg, appcache) and whether inline scripts are // 239
|
|
// currently allowed. // 240
|
|
// XXX so far this function is always called with arch === 'web.browser' // 241
|
|
var memoizedBoilerplate = {}; // 242
|
|
var getBoilerplate = function (request, arch) { // 243
|
|
// 244
|
|
var htmlAttributes = getHtmlAttributes(request); // 245
|
|
// 246
|
|
// The only thing that changes from request to request (for now) are // 247
|
|
// the HTML attributes (used by, eg, appcache) and whether inline // 248
|
|
// scripts are allowed, so we can memoize based on that. // 249
|
|
var memHash = JSON.stringify({ // 250
|
|
inlineScriptsAllowed: inlineScriptsAllowed, // 251
|
|
htmlAttributes: htmlAttributes, // 252
|
|
arch: arch // 253
|
|
}); // 254
|
|
// 255
|
|
if (! memoizedBoilerplate[memHash]) { // 256
|
|
memoizedBoilerplate[memHash] = boilerplateByArch[arch].toHTML({ // 257
|
|
htmlAttributes: htmlAttributes // 258
|
|
}); // 259
|
|
} // 260
|
|
return memoizedBoilerplate[memHash]; // 261
|
|
}; // 262
|
|
// 263
|
|
WebAppInternals.generateBoilerplateInstance = function (arch, // 264
|
|
manifest, // 265
|
|
additionalOptions) { // 266
|
|
additionalOptions = additionalOptions || {}; // 267
|
|
// 268
|
|
var runtimeConfig = _.extend( // 269
|
|
_.clone(__meteor_runtime_config__), // 270
|
|
additionalOptions.runtimeConfigOverrides || {} // 271
|
|
); // 272
|
|
// 273
|
|
var jsCssPrefix; // 274
|
|
if (arch === 'web.cordova') { // 275
|
|
// in cordova we serve assets up directly from disk so it doesn't make // 276
|
|
// sense to use the prefix (ordinarily something like a CDN) and go out // 277
|
|
// to the internet for those files. // 278
|
|
jsCssPrefix = ''; // 279
|
|
} else { // 280
|
|
jsCssPrefix = bundledJsCssPrefix || // 281
|
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; // 282
|
|
} // 283
|
|
// 284
|
|
return new Boilerplate(arch, manifest, // 285
|
|
_.extend({ // 286
|
|
pathMapper: function (itemPath) { // 287
|
|
return path.join(archPath[arch], itemPath); }, // 288
|
|
baseDataExtension: { // 289
|
|
additionalStaticJs: _.map( // 290
|
|
additionalStaticJs || [], // 291
|
|
function (contents, pathname) { // 292
|
|
return { // 293
|
|
pathname: pathname, // 294
|
|
contents: contents // 295
|
|
}; // 296
|
|
} // 297
|
|
), // 298
|
|
// Convert to a JSON string, then get rid of most weird characters, then // 299
|
|
// wrap in double quotes. (The outermost JSON.stringify really ought to // 300
|
|
// just be "wrap in double quotes" but we use it to be safe.) This might // 301
|
|
// end up inside a <script> tag so we need to be careful to not include // 302
|
|
// "</script>", but normal {{spacebars}} escaping escapes too much! See // 303
|
|
// https://github.com/meteor/meteor/issues/3730 // 304
|
|
meteorRuntimeConfig: JSON.stringify( // 305
|
|
encodeURIComponent(JSON.stringify(runtimeConfig))), // 306
|
|
rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '', // 307
|
|
bundledJsCssPrefix: jsCssPrefix, // 308
|
|
inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(), // 309
|
|
inline: additionalOptions.inline // 310
|
|
} // 311
|
|
}, additionalOptions) // 312
|
|
); // 313
|
|
}; // 314
|
|
// 315
|
|
// A mapping from url path to "info". Where "info" has the following fields: // 316
|
|
// - type: the type of file to be served // 317
|
|
// - cacheable: optionally, whether the file should be cached or not // 318
|
|
// - sourceMapUrl: optionally, the url of the source map // 319
|
|
// // 320
|
|
// Info also contains one of the following: // 321
|
|
// - content: the stringified content that should be served at this path // 322
|
|
// - absolutePath: the absolute path on disk to the file // 323
|
|
// 324
|
|
var staticFiles; // 325
|
|
// 326
|
|
// Serve static files from the manifest or added with // 327
|
|
// `addStaticJs`. Exported for tests. // 328
|
|
WebAppInternals.staticFilesMiddleware = function (staticFiles, req, res, next) { // 329
|
|
if ('GET' != req.method && 'HEAD' != req.method) { // 330
|
|
next(); // 331
|
|
return; // 332
|
|
} // 333
|
|
var pathname = connect.utils.parseUrl(req).pathname; // 334
|
|
try { // 335
|
|
pathname = decodeURIComponent(pathname); // 336
|
|
} catch (e) { // 337
|
|
next(); // 338
|
|
return; // 339
|
|
} // 340
|
|
// 341
|
|
var serveStaticJs = function (s) { // 342
|
|
res.writeHead(200, { // 343
|
|
'Content-type': 'application/javascript; charset=UTF-8' // 344
|
|
}); // 345
|
|
res.write(s); // 346
|
|
res.end(); // 347
|
|
}; // 348
|
|
// 349
|
|
if (pathname === "/meteor_runtime_config.js" && // 350
|
|
! WebAppInternals.inlineScriptsAllowed()) { // 351
|
|
serveStaticJs("__meteor_runtime_config__ = " + // 352
|
|
JSON.stringify(__meteor_runtime_config__) + ";"); // 353
|
|
return; // 354
|
|
} else if (_.has(additionalStaticJs, pathname) && // 355
|
|
! WebAppInternals.inlineScriptsAllowed()) { // 356
|
|
serveStaticJs(additionalStaticJs[pathname]); // 357
|
|
return; // 358
|
|
} // 359
|
|
// 360
|
|
if (!_.has(staticFiles, pathname)) { // 361
|
|
next(); // 362
|
|
return; // 363
|
|
} // 364
|
|
// 365
|
|
// We don't need to call pause because, unlike 'static', once we call into // 366
|
|
// 'send' and yield to the event loop, we never call another handler with // 367
|
|
// 'next'. // 368
|
|
// 369
|
|
var info = staticFiles[pathname]; // 370
|
|
// 371
|
|
// Cacheable files are files that should never change. Typically // 372
|
|
// named by their hash (eg meteor bundled js and css files). // 373
|
|
// We cache them ~forever (1yr). // 374
|
|
// // 375
|
|
// We cache non-cacheable files anyway. This isn't really correct, as users // 376
|
|
// can change the files and changes won't propagate immediately. However, if // 377
|
|
// we don't cache them, browsers will 'flicker' when rerendering // 378
|
|
// images. Eventually we will probably want to rewrite URLs of static assets // 379
|
|
// to include a query parameter to bust caches. That way we can both get // 380
|
|
// good caching behavior and allow users to change assets without delay. // 381
|
|
// https://github.com/meteor/meteor/issues/773 // 382
|
|
var maxAge = info.cacheable // 383
|
|
? 1000 * 60 * 60 * 24 * 365 // 384
|
|
: 1000 * 60 * 60 * 24; // 385
|
|
// 386
|
|
// Set the X-SourceMap header, which current Chrome, FireFox, and Safari // 387
|
|
// understand. (The SourceMap header is slightly more spec-correct but FF // 388
|
|
// doesn't understand it.) // 389
|
|
// // 390
|
|
// You may also need to enable source maps in Chrome: open dev tools, click // 391
|
|
// the gear in the bottom right corner, and select "enable source maps". // 392
|
|
if (info.sourceMapUrl) { // 393
|
|
res.setHeader('X-SourceMap', // 394
|
|
__meteor_runtime_config__.ROOT_URL_PATH_PREFIX + // 395
|
|
info.sourceMapUrl); // 396
|
|
} // 397
|
|
// 398
|
|
if (info.type === "js") { // 399
|
|
res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); // 400
|
|
} else if (info.type === "css") { // 401
|
|
res.setHeader("Content-Type", "text/css; charset=UTF-8"); // 402
|
|
} else if (info.type === "json") { // 403
|
|
res.setHeader("Content-Type", "application/json; charset=UTF-8"); // 404
|
|
// XXX if it is a manifest we are serving, set additional headers // 405
|
|
if (/\/manifest.json$/.test(pathname)) { // 406
|
|
res.setHeader("Access-Control-Allow-Origin", "*"); // 407
|
|
} // 408
|
|
} // 409
|
|
// 410
|
|
if (info.content) { // 411
|
|
res.write(info.content); // 412
|
|
res.end(); // 413
|
|
} else { // 414
|
|
send(req, info.absolutePath) // 415
|
|
.maxage(maxAge) // 416
|
|
.hidden(true) // if we specified a dotfile in the manifest, serve it // 417
|
|
.on('error', function (err) { // 418
|
|
Log.error("Error serving static file " + err); // 419
|
|
res.writeHead(500); // 420
|
|
res.end(); // 421
|
|
}) // 422
|
|
.on('directory', function () { // 423
|
|
Log.error("Unexpected directory " + info.absolutePath); // 424
|
|
res.writeHead(500); // 425
|
|
res.end(); // 426
|
|
}) // 427
|
|
.pipe(res); // 428
|
|
} // 429
|
|
}; // 430
|
|
// 431
|
|
var getUrlPrefixForArch = function (arch) { // 432
|
|
// XXX we rely on the fact that arch names don't contain slashes // 433
|
|
// in that case we would need to uri escape it // 434
|
|
// 435
|
|
// We add '__' to the beginning of non-standard archs to "scope" the url // 436
|
|
// to Meteor internals. // 437
|
|
return arch === WebApp.defaultArch ? // 438
|
|
'' : '/' + '__' + arch.replace(/^web\./, ''); // 439
|
|
}; // 440
|
|
// 441
|
|
var runWebAppServer = function () { // 442
|
|
var shuttingDown = false; // 443
|
|
var syncQueue = new Meteor._SynchronousQueue(); // 444
|
|
// 445
|
|
var getItemPathname = function (itemUrl) { // 446
|
|
return decodeURIComponent(url.parse(itemUrl).pathname); // 447
|
|
}; // 448
|
|
// 449
|
|
WebAppInternals.reloadClientPrograms = function () { // 450
|
|
syncQueue.runTask(function() { // 451
|
|
staticFiles = {}; // 452
|
|
var generateClientProgram = function (clientPath, arch) { // 453
|
|
// read the control for the client we'll be serving up // 454
|
|
var clientJsonPath = path.join(__meteor_bootstrap__.serverDir, // 455
|
|
clientPath); // 456
|
|
var clientDir = path.dirname(clientJsonPath); // 457
|
|
var clientJson = JSON.parse(readUtf8FileSync(clientJsonPath)); // 458
|
|
if (clientJson.format !== "web-program-pre1") // 459
|
|
throw new Error("Unsupported format for client assets: " + // 460
|
|
JSON.stringify(clientJson.format)); // 461
|
|
// 462
|
|
if (! clientJsonPath || ! clientDir || ! clientJson) // 463
|
|
throw new Error("Client config file not parsed."); // 464
|
|
// 465
|
|
var urlPrefix = getUrlPrefixForArch(arch); // 466
|
|
// 467
|
|
var manifest = clientJson.manifest; // 468
|
|
_.each(manifest, function (item) { // 469
|
|
if (item.url && item.where === "client") { // 470
|
|
staticFiles[urlPrefix + getItemPathname(item.url)] = { // 471
|
|
absolutePath: path.join(clientDir, item.path), // 472
|
|
cacheable: item.cacheable, // 473
|
|
// Link from source to its map // 474
|
|
sourceMapUrl: item.sourceMapUrl, // 475
|
|
type: item.type // 476
|
|
}; // 477
|
|
// 478
|
|
if (item.sourceMap) { // 479
|
|
// Serve the source map too, under the specified URL. We assume all // 480
|
|
// source maps are cacheable. // 481
|
|
staticFiles[urlPrefix + getItemPathname(item.sourceMapUrl)] = { // 482
|
|
absolutePath: path.join(clientDir, item.sourceMap), // 483
|
|
cacheable: true // 484
|
|
}; // 485
|
|
} // 486
|
|
} // 487
|
|
}); // 488
|
|
// 489
|
|
var program = { // 490
|
|
manifest: manifest, // 491
|
|
version: WebAppHashing.calculateClientHash(manifest, null, _.pick( // 492
|
|
__meteor_runtime_config__, 'PUBLIC_SETTINGS')), // 493
|
|
PUBLIC_SETTINGS: __meteor_runtime_config__.PUBLIC_SETTINGS // 494
|
|
}; // 495
|
|
// 496
|
|
WebApp.clientPrograms[arch] = program; // 497
|
|
// 498
|
|
// Serve the program as a string at /foo/<arch>/manifest.json // 499
|
|
// XXX change manifest.json -> program.json // 500
|
|
staticFiles[path.join(urlPrefix, 'manifest.json')] = { // 501
|
|
content: JSON.stringify(program), // 502
|
|
cacheable: true, // 503
|
|
type: "json" // 504
|
|
}; // 505
|
|
}; // 506
|
|
// 507
|
|
try { // 508
|
|
var clientPaths = __meteor_bootstrap__.configJson.clientPaths; // 509
|
|
_.each(clientPaths, function (clientPath, arch) { // 510
|
|
archPath[arch] = path.dirname(clientPath); // 511
|
|
generateClientProgram(clientPath, arch); // 512
|
|
}); // 513
|
|
// 514
|
|
// Exported for tests. // 515
|
|
WebAppInternals.staticFiles = staticFiles; // 516
|
|
} catch (e) { // 517
|
|
Log.error("Error reloading the client program: " + e.stack); // 518
|
|
process.exit(1); // 519
|
|
} // 520
|
|
}); // 521
|
|
}; // 522
|
|
// 523
|
|
WebAppInternals.generateBoilerplate = function () { // 524
|
|
// This boilerplate will be served to the mobile devices when used with // 525
|
|
// Meteor/Cordova for the Hot-Code Push and since the file will be served by // 526
|
|
// the device's server, it is important to set the DDP url to the actual // 527
|
|
// Meteor server accepting DDP connections and not the device's file server. // 528
|
|
var defaultOptionsForArch = { // 529
|
|
'web.cordova': { // 530
|
|
runtimeConfigOverrides: { // 531
|
|
// XXX We use absoluteUrl() here so that we serve https:// // 532
|
|
// URLs to cordova clients if force-ssl is in use. If we were // 533
|
|
// to use __meteor_runtime_config__.ROOT_URL instead of // 534
|
|
// absoluteUrl(), then Cordova clients would immediately get a // 535
|
|
// HCP setting their DDP_DEFAULT_CONNECTION_URL to // 536
|
|
// http://example.meteor.com. This breaks the app, because // 537
|
|
// force-ssl doesn't serve CORS headers on 302 // 538
|
|
// redirects. (Plus it's undesirable to have clients // 539
|
|
// connecting to http://example.meteor.com when force-ssl is // 540
|
|
// in use.) // 541
|
|
DDP_DEFAULT_CONNECTION_URL: process.env.MOBILE_DDP_URL || // 542
|
|
Meteor.absoluteUrl(), // 543
|
|
ROOT_URL: process.env.MOBILE_ROOT_URL || // 544
|
|
Meteor.absoluteUrl() // 545
|
|
} // 546
|
|
} // 547
|
|
}; // 548
|
|
// 549
|
|
syncQueue.runTask(function() { // 550
|
|
_.each(WebApp.clientPrograms, function (program, archName) { // 551
|
|
boilerplateByArch[archName] = // 552
|
|
WebAppInternals.generateBoilerplateInstance( // 553
|
|
archName, program.manifest, // 554
|
|
defaultOptionsForArch[archName]); // 555
|
|
}); // 556
|
|
// 557
|
|
// Clear the memoized boilerplate cache. // 558
|
|
memoizedBoilerplate = {}; // 559
|
|
// 560
|
|
// Configure CSS injection for the default arch // 561
|
|
// XXX implement the CSS injection for all archs? // 562
|
|
WebAppInternals.refreshableAssets = { // 563
|
|
allCss: boilerplateByArch[WebApp.defaultArch].baseData.css // 564
|
|
}; // 565
|
|
}); // 566
|
|
}; // 567
|
|
// 568
|
|
WebAppInternals.reloadClientPrograms(); // 569
|
|
// 570
|
|
// webserver // 571
|
|
var app = connect(); // 572
|
|
// 573
|
|
// Auto-compress any json, javascript, or text. // 574
|
|
app.use(connect.compress()); // 575
|
|
// 576
|
|
// Packages and apps can add handlers that run before any other Meteor // 577
|
|
// handlers via WebApp.rawConnectHandlers. // 578
|
|
var rawConnectHandlers = connect(); // 579
|
|
app.use(rawConnectHandlers); // 580
|
|
// 581
|
|
// We're not a proxy; reject (without crashing) attempts to treat us like // 582
|
|
// one. (See #1212.) // 583
|
|
app.use(function(req, res, next) { // 584
|
|
if (RoutePolicy.isValidUrl(req.url)) { // 585
|
|
next(); // 586
|
|
return; // 587
|
|
} // 588
|
|
res.writeHead(400); // 589
|
|
res.write("Not a proxy"); // 590
|
|
res.end(); // 591
|
|
}); // 592
|
|
// 593
|
|
// Strip off the path prefix, if it exists. // 594
|
|
app.use(function (request, response, next) { // 595
|
|
var pathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; // 596
|
|
var url = Npm.require('url').parse(request.url); // 597
|
|
var pathname = url.pathname; // 598
|
|
// check if the path in the url starts with the path prefix (and the part // 599
|
|
// after the path prefix must start with a / if it exists.) // 600
|
|
if (pathPrefix && pathname.substring(0, pathPrefix.length) === pathPrefix && // 601
|
|
(pathname.length == pathPrefix.length // 602
|
|
|| pathname.substring(pathPrefix.length, pathPrefix.length + 1) === "/")) { // 603
|
|
request.url = request.url.substring(pathPrefix.length); // 604
|
|
next(); // 605
|
|
} else if (pathname === "/favicon.ico" || pathname === "/robots.txt") { // 606
|
|
next(); // 607
|
|
} else if (pathPrefix) { // 608
|
|
response.writeHead(404); // 609
|
|
response.write("Unknown path"); // 610
|
|
response.end(); // 611
|
|
} else { // 612
|
|
next(); // 613
|
|
} // 614
|
|
}); // 615
|
|
// 616
|
|
// Parse the query string into res.query. Used by oauth_server, but it's // 617
|
|
// generally pretty handy.. // 618
|
|
app.use(connect.query()); // 619
|
|
// 620
|
|
// Serve static files from the manifest. // 621
|
|
// This is inspired by the 'static' middleware. // 622
|
|
app.use(function (req, res, next) { // 623
|
|
Fiber(function () { // 624
|
|
WebAppInternals.staticFilesMiddleware(staticFiles, req, res, next); // 625
|
|
}).run(); // 626
|
|
}); // 627
|
|
// 628
|
|
// Packages and apps can add handlers to this via WebApp.connectHandlers. // 629
|
|
// They are inserted before our default handler. // 630
|
|
var packageAndAppHandlers = connect(); // 631
|
|
app.use(packageAndAppHandlers); // 632
|
|
// 633
|
|
var suppressConnectErrors = false; // 634
|
|
// connect knows it is an error handler because it has 4 arguments instead of // 635
|
|
// 3. go figure. (It is not smart enough to find such a thing if it's hidden // 636
|
|
// inside packageAndAppHandlers.) // 637
|
|
app.use(function (err, req, res, next) { // 638
|
|
if (!err || !suppressConnectErrors || !req.headers['x-suppress-error']) { // 639
|
|
next(err); // 640
|
|
return; // 641
|
|
} // 642
|
|
res.writeHead(err.status, { 'Content-Type': 'text/plain' }); // 643
|
|
res.end("An error message"); // 644
|
|
}); // 645
|
|
// 646
|
|
app.use(function (req, res, next) { // 647
|
|
if (! appUrl(req.url)) // 648
|
|
return next(); // 649
|
|
// 650
|
|
var headers = { // 651
|
|
'Content-Type': 'text/html; charset=utf-8' // 652
|
|
}; // 653
|
|
if (shuttingDown) // 654
|
|
headers['Connection'] = 'Close'; // 655
|
|
// 656
|
|
var request = WebApp.categorizeRequest(req); // 657
|
|
// 658
|
|
if (request.url.query && request.url.query['meteor_css_resource']) { // 659
|
|
// In this case, we're requesting a CSS resource in the meteor-specific // 660
|
|
// way, but we don't have it. Serve a static css file that indicates that // 661
|
|
// we didn't have it, so we can detect that and refresh. // 662
|
|
headers['Content-Type'] = 'text/css; charset=utf-8'; // 663
|
|
res.writeHead(200, headers); // 664
|
|
res.write(".meteor-css-not-found-error { width: 0px;}"); // 665
|
|
res.end(); // 666
|
|
return undefined; // 667
|
|
} // 668
|
|
// 669
|
|
// /packages/asdfsad ... /__cordova/dafsdf.js // 670
|
|
var pathname = connect.utils.parseUrl(req).pathname; // 671
|
|
var archKey = pathname.split('/')[1]; // 672
|
|
var archKeyCleaned = 'web.' + archKey.replace(/^__/, ''); // 673
|
|
// 674
|
|
if (! /^__/.test(archKey) || ! _.has(archPath, archKeyCleaned)) { // 675
|
|
archKey = WebApp.defaultArch; // 676
|
|
} else { // 677
|
|
archKey = archKeyCleaned; // 678
|
|
} // 679
|
|
// 680
|
|
var boilerplate; // 681
|
|
try { // 682
|
|
boilerplate = getBoilerplate(request, archKey); // 683
|
|
} catch (e) { // 684
|
|
Log.error("Error running template: " + e); // 685
|
|
res.writeHead(500, headers); // 686
|
|
res.end(); // 687
|
|
return undefined; // 688
|
|
} // 689
|
|
// 690
|
|
res.writeHead(200, headers); // 691
|
|
res.write(boilerplate); // 692
|
|
res.end(); // 693
|
|
return undefined; // 694
|
|
}); // 695
|
|
// 696
|
|
// Return 404 by default, if no other handlers serve this URL. // 697
|
|
app.use(function (req, res) { // 698
|
|
res.writeHead(404); // 699
|
|
res.end(); // 700
|
|
}); // 701
|
|
// 702
|
|
// 703
|
|
var httpServer = http.createServer(app); // 704
|
|
var onListeningCallbacks = []; // 705
|
|
// 706
|
|
// After 5 seconds w/o data on a socket, kill it. On the other hand, if // 707
|
|
// there's an outstanding request, give it a higher timeout instead (to avoid // 708
|
|
// killing long-polling requests) // 709
|
|
httpServer.setTimeout(SHORT_SOCKET_TIMEOUT); // 710
|
|
// 711
|
|
// Do this here, and then also in livedata/stream_server.js, because // 712
|
|
// stream_server.js kills all the current request handlers when installing its // 713
|
|
// own. // 714
|
|
httpServer.on('request', WebApp._timeoutAdjustmentRequestCallback); // 715
|
|
// 716
|
|
// 717
|
|
// start up app // 718
|
|
_.extend(WebApp, { // 719
|
|
connectHandlers: packageAndAppHandlers, // 720
|
|
rawConnectHandlers: rawConnectHandlers, // 721
|
|
httpServer: httpServer, // 722
|
|
// For testing. // 723
|
|
suppressConnectErrors: function () { // 724
|
|
suppressConnectErrors = true; // 725
|
|
}, // 726
|
|
onListening: function (f) { // 727
|
|
if (onListeningCallbacks) // 728
|
|
onListeningCallbacks.push(f); // 729
|
|
else // 730
|
|
f(); // 731
|
|
} // 732
|
|
}); // 733
|
|
// 734
|
|
// Let the rest of the packages (and Meteor.startup hooks) insert connect // 735
|
|
// middlewares and update __meteor_runtime_config__, then keep going to set up // 736
|
|
// actually serving HTML. // 737
|
|
main = function (argv) { // 738
|
|
WebAppInternals.generateBoilerplate(); // 739
|
|
// 740
|
|
// only start listening after all the startup code has run. // 741
|
|
var localPort = parseInt(process.env.PORT) || 0; // 742
|
|
var host = process.env.BIND_IP; // 743
|
|
var localIp = host || '0.0.0.0'; // 744
|
|
httpServer.listen(localPort, localIp, Meteor.bindEnvironment(function() { // 745
|
|
if (process.env.METEOR_PRINT_ON_LISTEN) // 746
|
|
console.log("LISTENING"); // must match run-app.js // 747
|
|
// 748
|
|
var callbacks = onListeningCallbacks; // 749
|
|
onListeningCallbacks = null; // 750
|
|
_.each(callbacks, function (x) { x(); }); // 751
|
|
// 752
|
|
}, function (e) { // 753
|
|
console.error("Error listening:", e); // 754
|
|
console.error(e && e.stack); // 755
|
|
})); // 756
|
|
// 757
|
|
return 'DAEMON'; // 758
|
|
}; // 759
|
|
}; // 760
|
|
// 761
|
|
// 762
|
|
runWebAppServer(); // 763
|
|
// 764
|
|
// 765
|
|
var inlineScriptsAllowed = true; // 766
|
|
// 767
|
|
WebAppInternals.inlineScriptsAllowed = function () { // 768
|
|
return inlineScriptsAllowed; // 769
|
|
}; // 770
|
|
// 771
|
|
WebAppInternals.setInlineScriptsAllowed = function (value) { // 772
|
|
inlineScriptsAllowed = value; // 773
|
|
WebAppInternals.generateBoilerplate(); // 774
|
|
}; // 775
|
|
// 776
|
|
WebAppInternals.setBundledJsCssPrefix = function (prefix) { // 777
|
|
bundledJsCssPrefix = prefix; // 778
|
|
WebAppInternals.generateBoilerplate(); // 779
|
|
}; // 780
|
|
// 781
|
|
// Packages can call `WebAppInternals.addStaticJs` to specify static // 782
|
|
// JavaScript to be included in the app. This static JS will be inlined, // 783
|
|
// unless inline scripts have been disabled, in which case it will be // 784
|
|
// served under `/<sha1 of contents>`. // 785
|
|
var additionalStaticJs = {}; // 786
|
|
WebAppInternals.addStaticJs = function (contents) { // 787
|
|
additionalStaticJs["/" + sha1(contents) + ".js"] = contents; // 788
|
|
}; // 789
|
|
// 790
|
|
// Exported for tests // 791
|
|
WebAppInternals.getBoilerplate = getBoilerplate; // 792
|
|
WebAppInternals.additionalStaticJs = additionalStaticJs; // 793
|
|
// 794
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
}).call(this);
|
|
|
|
|
|
/* Exports */
|
|
if (typeof Package === 'undefined') Package = {};
|
|
Package.webapp = {
|
|
WebApp: WebApp,
|
|
main: main,
|
|
WebAppInternals: WebAppInternals
|
|
};
|
|
|
|
})();
|
|
|
|
//# sourceMappingURL=webapp.js.map
|