/* * Copyright 2014 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* eslint-disable object-shorthand */ 'use strict'; var http = require('http'); var path = require('path'); var fs = require('fs'); var mimeTypes = { '.css': 'text/css', '.html': 'text/html', '.js': 'application/javascript', '.json': 'application/json', '.svg': 'image/svg+xml', '.pdf': 'application/pdf', '.xhtml': 'application/xhtml+xml', '.gif': 'image/gif', '.ico': 'image/x-icon', '.png': 'image/png', '.log': 'text/plain', '.bcmap': 'application/octet-stream', '.properties': 'text/plain', }; var defaultMimeType = 'application/octet-stream'; function WebServer() { this.root = '.'; this.host = 'localhost'; this.port = 0; this.server = null; this.verbose = false; this.cacheExpirationTime = 0; this.disableRangeRequests = false; this.hooks = { 'GET': [crossOriginHandler], 'POST': [], }; } WebServer.prototype = { start: function (callback) { this._ensureNonZeroPort(); this.server = http.createServer(this._handler.bind(this)); this.server.listen(this.port, this.host, callback); console.log( 'Server running at http://' + this.host + ':' + this.port + '/'); }, stop: function (callback) { this.server.close(callback); this.server = null; }, _ensureNonZeroPort: function () { if (!this.port) { // If port is 0, a random port will be chosen instead. Do not set a host // name to make sure that the port is synchronously set by .listen(). var server = http.createServer().listen(0); var address = server.address(); // .address().port being available synchronously is merely an // implementation detail. So we are defensive here and fall back to some // fixed port when the address is not available yet. this.port = address ? address.port : 8000; server.close(); } }, _handler: function (req, res) { var url = req.url.replace(/\/\//g, '/'); var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url); var pathPart = decodeURI(urlParts[1]), queryPart = urlParts[3]; var verbose = this.verbose; var methodHooks = this.hooks[req.method]; if (!methodHooks) { res.writeHead(405); res.end('Unsupported request method', 'utf8'); return; } var handled = methodHooks.some(function (hook) { return hook(req, res); }); if (handled) { return; } if (pathPart === '/favicon.ico') { fs.realpath(path.join(this.root, 'test/resources/favicon.ico'), checkFile); return; } var disableRangeRequests = this.disableRangeRequests; var cacheExpirationTime = this.cacheExpirationTime; var filePath; fs.realpath(path.join(this.root, pathPart), checkFile); function checkFile(err, file) { if (err) { res.writeHead(404); res.end(); if (verbose) { console.error(url + ': not found'); } return; } filePath = file; fs.stat(filePath, statFile); } var fileSize; function statFile(err, stats) { if (err) { res.writeHead(500); res.end(); return; } fileSize = stats.size; var isDir = stats.isDirectory(); if (isDir && !/\/$/.test(pathPart)) { res.setHeader('Location', pathPart + '/' + urlParts[2]); res.writeHead(301); res.end('Redirected', 'utf8'); return; } if (isDir) { serveDirectoryIndex(filePath); return; } var range = req.headers['range']; if (range && !disableRangeRequests) { var rangesMatches = /^bytes=(\d+)\-(\d+)?/.exec(range); if (!rangesMatches) { res.writeHead(501); res.end('Bad range', 'utf8'); if (verbose) { console.error(url + ': bad range: "' + range + '"'); } return; } var start = +rangesMatches[1]; var end = +rangesMatches[2]; if (verbose) { console.log(url + ': range ' + start + ' - ' + end); } serveRequestedFileRange(filePath, start, isNaN(end) ? fileSize : (end + 1)); return; } if (verbose) { console.log(url); } serveRequestedFile(filePath); } function escapeHTML(untrusted) { // Escape untrusted input so that it can safely be used in a HTML response // in HTML and in HTML attributes. return untrusted .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function serveDirectoryIndex(dir) { res.setHeader('Content-Type', 'text/html'); res.writeHead(200); if (queryPart === 'frame') { res.end('
', 'utf8'); return; } var all = queryPart === 'all'; fs.readdir(dir, function (err, files) { if (err) { res.end(); return; } res.write('' + 'no files found
\n'); } if (!all && queryPart !== 'side') { res.write('(only PDF files are shown, ' + 'show all)
\n'); } res.end(''); }); } function serveRequestedFile(filePath) { var stream = fs.createReadStream(filePath, { flags: 'rs', }); stream.on('error', function (error) { res.writeHead(500); res.end(); }); var ext = path.extname(filePath).toLowerCase(); var contentType = mimeTypes[ext] || defaultMimeType; if (!disableRangeRequests) { res.setHeader('Accept-Ranges', 'bytes'); } res.setHeader('Content-Type', contentType); res.setHeader('Content-Length', fileSize); if (cacheExpirationTime > 0) { var expireTime = new Date(); expireTime.setSeconds(expireTime.getSeconds() + cacheExpirationTime); res.setHeader('Expires', expireTime.toUTCString()); } res.writeHead(200); stream.pipe(res); } function serveRequestedFileRange(filePath, start, end) { var stream = fs.createReadStream(filePath, { flags: 'rs', start: start, end: end - 1, }); stream.on('error', function (error) { res.writeHead(500); res.end(); }); var ext = path.extname(filePath).toLowerCase(); var contentType = mimeTypes[ext] || defaultMimeType; res.setHeader('Accept-Ranges', 'bytes'); res.setHeader('Content-Type', contentType); res.setHeader('Content-Length', (end - start)); res.setHeader('Content-Range', 'bytes ' + start + '-' + (end - 1) + '/' + fileSize); res.writeHead(206); stream.pipe(res); } }, }; // This supports the "Cross-origin" test in test/unit/api_spec.js // It is here instead of test.js so that when the test will still complete as // expected if the user does "gulp server" and then visits // http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin function crossOriginHandler(req, res) { if (req.url === '/test/pdfs/basicapi.pdf?cors=withCredentials') { res.setHeader('Access-Control-Allow-Origin', req.headers['origin']); res.setHeader('Access-Control-Allow-Credentials', 'true'); } if (req.url === '/test/pdfs/basicapi.pdf?cors=withoutCredentials') { res.setHeader('Access-Control-Allow-Origin', req.headers['origin']); } } exports.WebServer = WebServer;