////////// Requires ////////// var fs = Npm.require("fs"); var http = Npm.require("http"); var os = Npm.require("os"); var path = Npm.require("path"); var url = Npm.require("url"); var crypto = Npm.require("crypto"); var connect = Npm.require('connect'); var parseurl = Npm.require('parseurl'); var useragent = Npm.require('useragent'); var send = Npm.require('send'); var Future = Npm.require('fibers/future'); var Fiber = Npm.require('fibers'); var SHORT_SOCKET_TIMEOUT = 5*1000; var LONG_SOCKET_TIMEOUT = 120*1000; WebApp = {}; WebAppInternals = {}; WebAppInternals.NpmModules = { connect: { version: Npm.require('connect/package.json').version, module: connect } }; WebApp.defaultArch = 'web.browser'; // XXX maps archs to manifests WebApp.clientPrograms = {}; // XXX maps archs to program path on filesystem var archPath = {}; var bundledJsCssUrlRewriteHook = function (url) { var bundledPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; return bundledPrefix + url; }; var sha1 = function (contents) { var hash = crypto.createHash('sha1'); hash.update(contents); return hash.digest('hex'); }; var readUtf8FileSync = function (filename) { return Meteor.wrapAsync(fs.readFile)(filename, 'utf8'); }; // #BrowserIdentification // // We have multiple places that want to identify the browser: the // unsupported browser page, the appcache package, and, eventually // delivering browser polyfills only as needed. // // To avoid detecting the browser in multiple places ad-hoc, we create a // Meteor "browser" object. It uses but does not expose the npm // useragent module (we could choose a different mechanism to identify // the browser in the future if we wanted to). The browser object // contains // // * `name`: the name of the browser in camel case // * `major`, `minor`, `patch`: integers describing the browser version // // Also here is an early version of a Meteor `request` object, intended // to be a high-level description of the request without exposing // details of connect's low-level `req`. Currently it contains: // // * `browser`: browser identification object described above // * `url`: parsed url, including parsed query params // // As a temporary hack there is a `categorizeRequest` function on WebApp which // converts a connect `req` to a Meteor `request`. This can go away once smart // packages such as appcache are being passed a `request` object directly when // they serve content. // // This allows `request` to be used uniformly: it is passed to the html // attributes hook, and the appcache package can use it when deciding // whether to generate a 404 for the manifest. // // Real routing / server side rendering will probably refactor this // heavily. // e.g. "Mobile Safari" => "mobileSafari" var camelCase = function (name) { var parts = name.split(' '); parts[0] = parts[0].toLowerCase(); for (var i = 1; i < parts.length; ++i) { parts[i] = parts[i].charAt(0).toUpperCase() + parts[i].substr(1); } return parts.join(''); }; var identifyBrowser = function (userAgentString) { var userAgent = useragent.lookup(userAgentString); return { name: camelCase(userAgent.family), major: +userAgent.major, minor: +userAgent.minor, patch: +userAgent.patch }; }; // XXX Refactor as part of implementing real routing. WebAppInternals.identifyBrowser = identifyBrowser; WebApp.categorizeRequest = function (req) { return _.extend({ browser: identifyBrowser(req.headers['user-agent']), url: url.parse(req.url, true) }, _.pick(req, 'dynamicHead', 'dynamicBody')); }; // HTML attribute hooks: functions to be called to determine any attributes to // be added to the '' tag. Each function is passed a 'request' object (see // #BrowserIdentification) and should return null or object. var htmlAttributeHooks = []; var getHtmlAttributes = function (request) { var combinedAttributes = {}; _.each(htmlAttributeHooks || [], function (hook) { var attributes = hook(request); if (attributes === null) return; if (typeof attributes !== 'object') throw Error("HTML attribute hook must return null or object"); _.extend(combinedAttributes, attributes); }); return combinedAttributes; }; WebApp.addHtmlAttributeHook = function (hook) { htmlAttributeHooks.push(hook); }; // 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 routes such as /sockjs/. if (RoutePolicy.classify(url)) return false; // we currently return app HTML on all URLs by default return true; }; // We need to calculate the client hash after all packages have loaded // to give them a chance to populate __meteor_runtime_config__. // // Calculating the hash during startup means that packages can only // populate __meteor_runtime_config__ during load, not during startup. // // Calculating instead it at the beginning of main after all startup // hooks had run would allow packages to also populate // __meteor_runtime_config__ during startup, but that's too late for // autoupdate because it needs to have the client hash at startup to // insert the auto update version itself into // __meteor_runtime_config__ to get it to the client. // // An alternative would be to give autoupdate a "post-start, // pre-listen" hook to allow it to insert the auto update version at // the right moment. Meteor.startup(function () { var calculateClientHash = WebAppHashing.calculateClientHash; WebApp.clientHash = function (archName) { archName = archName || WebApp.defaultArch; return calculateClientHash(WebApp.clientPrograms[archName].manifest); }; WebApp.calculateClientHashRefreshable = function (archName) { archName = archName || WebApp.defaultArch; return calculateClientHash(WebApp.clientPrograms[archName].manifest, function (name) { return name === "css"; }); }; WebApp.calculateClientHashNonRefreshable = function (archName) { archName = archName || WebApp.defaultArch; return calculateClientHash(WebApp.clientPrograms[archName].manifest, function (name) { return name !== "css"; }); }; WebApp.calculateClientHashCordova = function () { var archName = 'web.cordova'; if (! WebApp.clientPrograms[archName]) return 'none'; return calculateClientHash( WebApp.clientPrograms[archName].manifest, null, _.pick( __meteor_runtime_config__, 'PUBLIC_SETTINGS')); }; }); // When we have a request pending, we want the socket timeout to be long, to // give ourselves a while to serve it, and to allow sockjs long polls to // complete. On the other hand, we want to close idle sockets relatively // quickly, so that we can shut down relatively promptly but cleanly, without // cutting off anyone's response. WebApp._timeoutAdjustmentRequestCallback = function (req, res) { // this is really just req.socket.setTimeout(LONG_SOCKET_TIMEOUT); req.setTimeout(LONG_SOCKET_TIMEOUT); // Insert our new finish listener to run BEFORE the existing one which removes // the response from the socket. var finishListeners = res.listeners('finish'); // XXX Apparently in Node 0.12 this event was called 'prefinish'. // https://github.com/joyent/node/commit/7c9b6070 // But it has switched back to 'finish' in Node v4: // https://github.com/nodejs/node/pull/1411 res.removeAllListeners('finish'); res.on('finish', function () { res.setTimeout(SHORT_SOCKET_TIMEOUT); }); _.each(finishListeners, function (l) { res.on('finish', l); }); }; // Will be updated by main before we listen. // Map from client arch to boilerplate object. // Boilerplate object has: // - func: XXX // - baseData: XXX var boilerplateByArch = {}; // Given a request (as returned from `categorizeRequest`), return the // boilerplate HTML to serve for that request. // // If a previous connect middleware has rendered content for the head or body, // returns the boilerplate with that content patched in otherwise // memoizes on HTML attributes (used by, eg, appcache) and whether inline // scripts are currently allowed. // XXX so far this function is always called with arch === 'web.browser' var memoizedBoilerplate = {}; var getBoilerplate = function (request, arch) { var useMemoized = ! (request.dynamicHead || request.dynamicBody); var htmlAttributes = getHtmlAttributes(request); if (useMemoized) { // The only thing that changes from request to request (unless extra // content is added to the head or body) are the HTML attributes // (used by, eg, appcache) and whether inline scripts are allowed, so we // can memoize based on that. var memHash = JSON.stringify({ inlineScriptsAllowed: inlineScriptsAllowed, htmlAttributes: htmlAttributes, arch: arch }); if (! memoizedBoilerplate[memHash]) { memoizedBoilerplate[memHash] = boilerplateByArch[arch].toHTML({ htmlAttributes: htmlAttributes }); } return memoizedBoilerplate[memHash]; } var boilerplateOptions = _.extend({ htmlAttributes: htmlAttributes }, _.pick(request, 'dynamicHead', 'dynamicBody')); return boilerplateByArch[arch].toHTML(boilerplateOptions); }; WebAppInternals.generateBoilerplateInstance = function (arch, manifest, additionalOptions) { additionalOptions = additionalOptions || {}; var runtimeConfig = _.extend( _.clone(__meteor_runtime_config__), additionalOptions.runtimeConfigOverrides || {} ); return new Boilerplate(arch, manifest, _.extend({ pathMapper: function (itemPath) { return path.join(archPath[arch], itemPath); }, baseDataExtension: { additionalStaticJs: _.map( additionalStaticJs || [], function (contents, pathname) { return { pathname: pathname, contents: contents }; } ), // Convert to a JSON string, then get rid of most weird characters, then // wrap in double quotes. (The outermost JSON.stringify really ought to // just be "wrap in double quotes" but we use it to be safe.) This might // end up inside a ", but normal {{spacebars}} escaping escapes too much! See // https://github.com/meteor/meteor/issues/3730 meteorRuntimeConfig: JSON.stringify( encodeURIComponent(JSON.stringify(runtimeConfig))), rootUrlPathPrefix: __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '', bundledJsCssUrlRewriteHook: bundledJsCssUrlRewriteHook, inlineScriptsAllowed: WebAppInternals.inlineScriptsAllowed(), inline: additionalOptions.inline } }, additionalOptions) ); }; // A mapping from url path to "info". Where "info" has the following fields: // - type: the type of file to be served // - cacheable: optionally, whether the file should be cached or not // - sourceMapUrl: optionally, the url of the source map // // Info also contains one of the following: // - content: the stringified content that should be served at this path // - absolutePath: the absolute path on disk to the file var staticFiles; // Serve static files from the manifest or added with // `addStaticJs`. Exported for tests. WebAppInternals.staticFilesMiddleware = function (staticFiles, req, res, next) { if ('GET' != req.method && 'HEAD' != req.method && 'OPTIONS' != req.method) { next(); return; } var pathname = parseurl(req).pathname; try { pathname = decodeURIComponent(pathname); } catch (e) { next(); return; } var serveStaticJs = function (s) { res.writeHead(200, { 'Content-type': 'application/javascript; charset=UTF-8' }); res.write(s); res.end(); }; if (pathname === "/meteor_runtime_config.js" && ! WebAppInternals.inlineScriptsAllowed()) { serveStaticJs("__meteor_runtime_config__ = " + JSON.stringify(__meteor_runtime_config__) + ";"); return; } else if (_.has(additionalStaticJs, pathname) && ! WebAppInternals.inlineScriptsAllowed()) { serveStaticJs(additionalStaticJs[pathname]); return; } if (!_.has(staticFiles, pathname)) { next(); return; } // We don't need to call pause because, unlike 'static', once we call into // 'send' and yield to the event loop, we never call another handler with // 'next'. var info = staticFiles[pathname]; // Cacheable files are files that should never change. Typically // named by their hash (eg meteor bundled js and css files). // We cache them ~forever (1yr). var maxAge = info.cacheable ? 1000 * 60 * 60 * 24 * 365 : 0; // Set the X-SourceMap header, which current Chrome, FireFox, and Safari // understand. (The SourceMap header is slightly more spec-correct but FF // doesn't understand it.) // // You may also need to enable source maps in Chrome: open dev tools, click // the gear in the bottom right corner, and select "enable source maps". if (info.sourceMapUrl) { res.setHeader('X-SourceMap', __meteor_runtime_config__.ROOT_URL_PATH_PREFIX + info.sourceMapUrl); } if (info.type === "js") { res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); } else if (info.type === "css") { res.setHeader("Content-Type", "text/css; charset=UTF-8"); } else if (info.type === "json") { res.setHeader("Content-Type", "application/json; charset=UTF-8"); } if (info.hash) { res.setHeader('ETag', '"' + info.hash + '"'); } if (info.content) { res.write(info.content); res.end(); } else { send(req, info.absolutePath, { maxage: maxAge, dotfiles: 'allow', // if we specified a dotfile in the manifest, serve it lastModified: false // don't set last-modified based on the file date }).on('error', function (err) { Log.error("Error serving static file " + err); res.writeHead(500); res.end(); }) .on('directory', function () { Log.error("Unexpected directory " + info.absolutePath); res.writeHead(500); res.end(); }) .pipe(res); } }; var getUrlPrefixForArch = function (arch) { // XXX we rely on the fact that arch names don't contain slashes // in that case we would need to uri escape it // We add '__' to the beginning of non-standard archs to "scope" the url // to Meteor internals. return arch === WebApp.defaultArch ? '' : '/' + '__' + arch.replace(/^web\./, ''); }; // parse port to see if its a Windows Server style named pipe. If so, return as-is (String), otherwise return as Int WebAppInternals.parsePort = function (port) { if( /\\\\?.+\\pipe\\?.+/.test(port) ) { return port; } return parseInt(port); }; var runWebAppServer = function () { var shuttingDown = false; var syncQueue = new Meteor._SynchronousQueue(); var getItemPathname = function (itemUrl) { return decodeURIComponent(url.parse(itemUrl).pathname); }; WebAppInternals.reloadClientPrograms = function () { syncQueue.runTask(function() { staticFiles = {}; var generateClientProgram = function (clientPath, arch) { // read the control for the client we'll be serving up var clientJsonPath = path.join(__meteor_bootstrap__.serverDir, clientPath); var clientDir = path.dirname(clientJsonPath); var clientJson = JSON.parse(readUtf8FileSync(clientJsonPath)); if (clientJson.format !== "web-program-pre1") throw new Error("Unsupported format for client assets: " + JSON.stringify(clientJson.format)); if (! clientJsonPath || ! clientDir || ! clientJson) throw new Error("Client config file not parsed."); var urlPrefix = getUrlPrefixForArch(arch); var manifest = clientJson.manifest; _.each(manifest, function (item) { if (item.url && item.where === "client") { staticFiles[urlPrefix + getItemPathname(item.url)] = { absolutePath: path.join(clientDir, item.path), cacheable: item.cacheable, hash: item.hash, // Link from source to its map sourceMapUrl: item.sourceMapUrl, type: item.type }; if (item.sourceMap) { // Serve the source map too, under the specified URL. We assume all // source maps are cacheable. staticFiles[urlPrefix + getItemPathname(item.sourceMapUrl)] = { absolutePath: path.join(clientDir, item.sourceMap), cacheable: true }; } } }); var program = { format: "web-program-pre1", manifest: manifest, version: WebAppHashing.calculateClientHash(manifest, null, _.pick( __meteor_runtime_config__, 'PUBLIC_SETTINGS')), cordovaCompatibilityVersions: clientJson.cordovaCompatibilityVersions, PUBLIC_SETTINGS: __meteor_runtime_config__.PUBLIC_SETTINGS }; WebApp.clientPrograms[arch] = program; // Serve the program as a string at /foo//manifest.json // XXX change manifest.json -> program.json staticFiles[urlPrefix + getItemPathname('/manifest.json')] = { content: JSON.stringify(program), cacheable: false, hash: program.version, type: "json" }; }; try { var clientPaths = __meteor_bootstrap__.configJson.clientPaths; _.each(clientPaths, function (clientPath, arch) { archPath[arch] = path.dirname(clientPath); generateClientProgram(clientPath, arch); }); // Exported for tests. WebAppInternals.staticFiles = staticFiles; } catch (e) { Log.error("Error reloading the client program: " + e.stack); process.exit(1); } }); }; WebAppInternals.generateBoilerplate = function () { // This boilerplate will be served to the mobile devices when used with // Meteor/Cordova for the Hot-Code Push and since the file will be served by // the device's server, it is important to set the DDP url to the actual // Meteor server accepting DDP connections and not the device's file server. var defaultOptionsForArch = { 'web.cordova': { runtimeConfigOverrides: { // XXX We use absoluteUrl() here so that we serve https:// // URLs to cordova clients if force-ssl is in use. If we were // to use __meteor_runtime_config__.ROOT_URL instead of // absoluteUrl(), then Cordova clients would immediately get a // HCP setting their DDP_DEFAULT_CONNECTION_URL to // http://example.meteor.com. This breaks the app, because // force-ssl doesn't serve CORS headers on 302 // redirects. (Plus it's undesirable to have clients // connecting to http://example.meteor.com when force-ssl is // in use.) DDP_DEFAULT_CONNECTION_URL: process.env.MOBILE_DDP_URL || Meteor.absoluteUrl(), ROOT_URL: process.env.MOBILE_ROOT_URL || Meteor.absoluteUrl() } } }; syncQueue.runTask(function() { _.each(WebApp.clientPrograms, function (program, archName) { boilerplateByArch[archName] = WebAppInternals.generateBoilerplateInstance( archName, program.manifest, defaultOptionsForArch[archName]); }); // Clear the memoized boilerplate cache. memoizedBoilerplate = {}; // Configure CSS injection for the default arch // XXX implement the CSS injection for all archs? var cssFiles = boilerplateByArch[WebApp.defaultArch].baseData.css; // Rewrite all CSS files (which are written directly to