123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- /*
- * 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 no-var */
- "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(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(callback) {
- this.server.close(callback);
- this.server = null;
- },
- _ensureNonZeroPort() {
- 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(req, res) {
- var url = req.url.replace(/\/\//g, "/");
- var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
- try {
- // Guard against directory traversal attacks such as
- // `/../../../../../../../etc/passwd`, which let you make GET requests
- // for files outside of `this.root`.
- var pathPart = path.normalize(decodeURI(urlParts[1]));
- // path.normalize returns a path on the basis of the current platform.
- // Windows paths cause issues in statFile and serverDirectoryIndex.
- // Converting to unix path would avoid platform checks in said functions.
- pathPart = pathPart.replace(/\\/g, "/");
- } catch (ex) {
- // If the URI cannot be decoded, a `URIError` is thrown. This happens for
- // malformed URIs such as `http://localhost:8888/%s%s` and should be
- // handled as a bad request.
- res.writeHead(400);
- res.end("Bad request", "utf8");
- return;
- }
- var 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, """)
- .replace(/'/g, "'");
- }
- function serveDirectoryIndex(dir) {
- res.setHeader("Content-Type", "text/html");
- res.writeHead(200);
- if (queryPart === "frame") {
- res.end(
- "<html><frameset cols=*,200><frame name=pdf>" +
- '<frame src="' +
- encodeURI(pathPart) +
- '?side"></frameset></html>',
- "utf8"
- );
- return;
- }
- var all = queryPart === "all";
- fs.readdir(dir, function (err, files) {
- if (err) {
- res.end();
- return;
- }
- res.write(
- '<html><head><meta charset="utf-8"></head><body>' +
- "<h1>PDFs of " +
- pathPart +
- "</h1>\n"
- );
- if (pathPart !== "/") {
- res.write('<a href="..">..</a><br>\n');
- }
- files.forEach(function (file) {
- var stat;
- var item = pathPart + file;
- var href = "";
- var label = "";
- var extraAttributes = "";
- try {
- stat = fs.statSync(path.join(dir, file));
- } catch (e) {
- href = encodeURI(item);
- label = file + " (" + e + ")";
- extraAttributes = ' style="color:red"';
- }
- if (stat) {
- if (stat.isDirectory()) {
- href = encodeURI(item);
- label = file;
- } else if (path.extname(file).toLowerCase() === ".pdf") {
- href = "/web/viewer.html?file=" + encodeURIComponent(item);
- label = file;
- extraAttributes = ' target="pdf"';
- } else if (all) {
- href = encodeURI(item);
- label = file;
- }
- }
- if (label) {
- res.write(
- '<a href="' +
- escapeHTML(href) +
- '"' +
- extraAttributes +
- ">" +
- escapeHTML(label) +
- "</a><br>\n"
- );
- }
- });
- if (files.length === 0) {
- res.write("<p>no files found</p>\n");
- }
- if (!all && queryPart !== "side") {
- res.write(
- "<hr><p>(only PDF files are shown, " +
- '<a href="?all">show all</a>)</p>\n'
- );
- }
- res.end("</body></html>");
- });
- }
- function serveRequestedFile(reqFilePath) {
- var stream = fs.createReadStream(reqFilePath, { flags: "rs" });
- stream.on("error", function (error) {
- res.writeHead(500);
- res.end();
- });
- var ext = path.extname(reqFilePath).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(reqFilePath, start, end) {
- var stream = fs.createReadStream(reqFilePath, {
- flags: "rs",
- start,
- end: end - 1,
- });
- stream.on("error", function (error) {
- res.writeHead(500);
- res.end();
- });
- var ext = path.extname(reqFilePath).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;
|