2014-03-22 09:47:23 +09:00
|
|
|
/*
|
|
|
|
* 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.
|
|
|
|
*/
|
2021-03-13 23:51:44 +09:00
|
|
|
/* eslint-disable no-var */
|
2014-03-22 09:47:23 +09:00
|
|
|
|
2023-07-08 19:44:37 +09:00
|
|
|
import fs from "fs";
|
|
|
|
import http from "http";
|
|
|
|
import path from "path";
|
2014-03-22 09:47:23 +09:00
|
|
|
|
|
|
|
var mimeTypes = {
|
|
|
|
".css": "text/css",
|
|
|
|
".html": "text/html",
|
|
|
|
".js": "application/javascript",
|
2023-07-08 19:44:37 +09:00
|
|
|
".mjs": "application/javascript",
|
2014-03-22 09:47:23 +09:00
|
|
|
".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",
|
2023-10-13 23:23:17 +09:00
|
|
|
".ftl": "text/plain",
|
2014-03-22 09:47:23 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
var defaultMimeType = "application/octet-stream";
|
|
|
|
|
2024-02-11 21:37:01 +09:00
|
|
|
class WebServer {
|
|
|
|
constructor() {
|
|
|
|
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: [],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-01-23 01:38:26 +09:00
|
|
|
start(callback) {
|
2024-02-11 21:37:01 +09:00
|
|
|
this.#ensureNonZeroPort();
|
|
|
|
this.server = http.createServer(this.#handler.bind(this));
|
2014-03-22 09:47:23 +09:00
|
|
|
this.server.listen(this.port, this.host, callback);
|
|
|
|
console.log(
|
|
|
|
"Server running at http://" + this.host + ":" + this.port + "/"
|
|
|
|
);
|
2024-02-11 21:37:01 +09:00
|
|
|
}
|
|
|
|
|
2021-01-23 01:38:26 +09:00
|
|
|
stop(callback) {
|
2014-03-22 09:47:23 +09:00
|
|
|
this.server.close(callback);
|
|
|
|
this.server = null;
|
2024-02-11 21:37:01 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
#ensureNonZeroPort() {
|
2015-11-11 03:08:52 +09:00
|
|
|
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();
|
|
|
|
}
|
2024-02-11 21:37:01 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
#handler(req, res) {
|
2024-02-11 21:45:06 +09:00
|
|
|
var self = this;
|
2023-03-23 20:34:08 +09:00
|
|
|
var url = req.url.replaceAll("//", "/");
|
2014-03-22 09:47:23 +09:00
|
|
|
var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
|
2019-01-13 22:50:27 +09:00
|
|
|
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]));
|
2019-07-07 04:14:28 +09:00
|
|
|
// 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.
|
2023-03-23 20:34:08 +09:00
|
|
|
pathPart = pathPart.replaceAll("\\", "/");
|
2023-06-12 18:46:11 +09:00
|
|
|
} catch {
|
2019-01-13 22:50:27 +09:00
|
|
|
// 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;
|
|
|
|
}
|
2018-12-11 03:59:04 +09:00
|
|
|
var queryPart = urlParts[3];
|
2014-03-22 09:47:23 +09:00
|
|
|
var verbose = this.verbose;
|
|
|
|
|
|
|
|
var methodHooks = this.hooks[req.method];
|
|
|
|
if (!methodHooks) {
|
|
|
|
res.writeHead(405);
|
|
|
|
res.end("Unsupported request method", "utf8");
|
|
|
|
return;
|
|
|
|
}
|
2020-04-14 19:28:14 +09:00
|
|
|
var handled = methodHooks.some(function (hook) {
|
2014-03-22 09:47:23 +09:00
|
|
|
return hook(req, res);
|
|
|
|
});
|
|
|
|
if (handled) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (pathPart === "/favicon.ico") {
|
|
|
|
fs.realpath(
|
|
|
|
path.join(this.root, "test/resources/favicon.ico"),
|
|
|
|
checkFile
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2014-03-31 21:46:11 +09:00
|
|
|
var disableRangeRequests = this.disableRangeRequests;
|
2014-03-26 04:53:40 +09:00
|
|
|
|
2014-03-22 09:47:23 +09:00
|
|
|
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)) {
|
2021-08-03 18:14:49 +09:00
|
|
|
res.setHeader("Location", pathPart + "/" + urlParts[2]);
|
2014-03-22 09:47:23 +09:00
|
|
|
res.writeHead(301);
|
|
|
|
res.end("Redirected", "utf8");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (isDir) {
|
|
|
|
serveDirectoryIndex(filePath);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-04-17 19:06:27 +09:00
|
|
|
var range = req.headers.range;
|
2014-03-26 04:53:40 +09:00
|
|
|
if (range && !disableRangeRequests) {
|
2020-11-07 20:59:53 +09:00
|
|
|
var rangesMatches = /^bytes=(\d+)-(\d+)?/.exec(range);
|
2014-03-22 09:47:23 +09:00
|
|
|
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);
|
|
|
|
}
|
2024-02-11 21:49:42 +09:00
|
|
|
self.#serveFileRange(
|
|
|
|
res,
|
2014-03-22 09:47:23 +09:00
|
|
|
filePath,
|
2024-02-11 21:49:42 +09:00
|
|
|
fileSize,
|
2014-03-22 09:47:23 +09:00
|
|
|
start,
|
|
|
|
isNaN(end) ? fileSize : end + 1
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (verbose) {
|
|
|
|
console.log(url);
|
|
|
|
}
|
2024-02-11 21:45:06 +09:00
|
|
|
self.#serveFile(res, filePath, fileSize);
|
2014-03-22 09:47:23 +09:00
|
|
|
}
|
|
|
|
|
2015-11-07 05:52:35 +09:00
|
|
|
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
|
2023-03-23 20:34:08 +09:00
|
|
|
.replaceAll("&", "&")
|
|
|
|
.replaceAll("<", "<")
|
|
|
|
.replaceAll(">", ">")
|
|
|
|
.replaceAll('"', """)
|
|
|
|
.replaceAll("'", "'");
|
2015-11-07 05:52:35 +09:00
|
|
|
}
|
|
|
|
|
2014-03-22 09:47:23 +09:00
|
|
|
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";
|
2020-04-14 19:28:14 +09:00
|
|
|
fs.readdir(dir, function (err, files) {
|
2014-03-22 09:47:23 +09:00
|
|
|
if (err) {
|
|
|
|
res.end();
|
|
|
|
return;
|
|
|
|
}
|
2015-01-08 20:22:34 +09:00
|
|
|
res.write(
|
|
|
|
'<html><head><meta charset="utf-8"></head><body>' +
|
|
|
|
"<h1>PDFs of " +
|
|
|
|
pathPart +
|
|
|
|
"</h1>\n"
|
|
|
|
);
|
2014-03-22 09:47:23 +09:00
|
|
|
if (pathPart !== "/") {
|
|
|
|
res.write('<a href="..">..</a><br>\n');
|
|
|
|
}
|
2020-04-14 19:28:14 +09:00
|
|
|
files.forEach(function (file) {
|
2015-11-07 05:52:35 +09:00
|
|
|
var stat;
|
2014-03-22 09:47:23 +09:00
|
|
|
var item = pathPart + file;
|
2015-11-07 05:52:35 +09:00
|
|
|
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;
|
|
|
|
}
|
2014-03-22 09:47:23 +09:00
|
|
|
}
|
2015-11-07 05:52:35 +09:00
|
|
|
if (label) {
|
|
|
|
res.write(
|
|
|
|
'<a href="' +
|
|
|
|
escapeHTML(href) +
|
|
|
|
'"' +
|
|
|
|
extraAttributes +
|
|
|
|
">" +
|
|
|
|
escapeHTML(label) +
|
|
|
|
"</a><br>\n"
|
|
|
|
);
|
2014-03-22 09:47:23 +09:00
|
|
|
}
|
|
|
|
});
|
|
|
|
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>");
|
|
|
|
});
|
|
|
|
}
|
2024-02-11 21:37:01 +09:00
|
|
|
}
|
2024-02-11 21:45:06 +09:00
|
|
|
|
|
|
|
#serveFile(response, filePath, fileSize) {
|
|
|
|
const stream = fs.createReadStream(filePath, { flags: "rs" });
|
|
|
|
stream.on("error", error => {
|
|
|
|
response.writeHead(500);
|
|
|
|
response.end();
|
|
|
|
});
|
|
|
|
|
|
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
|
|
const contentType = mimeTypes[extension] || defaultMimeType;
|
|
|
|
|
|
|
|
if (!this.disableRangeRequests) {
|
|
|
|
response.setHeader("Accept-Ranges", "bytes");
|
|
|
|
}
|
|
|
|
response.setHeader("Content-Type", contentType);
|
|
|
|
response.setHeader("Content-Length", fileSize);
|
|
|
|
if (this.cacheExpirationTime > 0) {
|
|
|
|
const expireTime = new Date();
|
|
|
|
expireTime.setSeconds(expireTime.getSeconds() + this.cacheExpirationTime);
|
|
|
|
response.setHeader("Expires", expireTime.toUTCString());
|
|
|
|
}
|
|
|
|
response.writeHead(200);
|
|
|
|
stream.pipe(response);
|
|
|
|
}
|
2024-02-11 21:49:42 +09:00
|
|
|
|
|
|
|
#serveFileRange(response, filePath, fileSize, start, end) {
|
|
|
|
const stream = fs.createReadStream(filePath, {
|
|
|
|
flags: "rs",
|
|
|
|
start,
|
|
|
|
end: end - 1,
|
|
|
|
});
|
|
|
|
stream.on("error", error => {
|
|
|
|
response.writeHead(500);
|
|
|
|
response.end();
|
|
|
|
});
|
|
|
|
|
|
|
|
const extension = path.extname(filePath).toLowerCase();
|
|
|
|
const contentType = mimeTypes[extension] || defaultMimeType;
|
|
|
|
|
|
|
|
response.setHeader("Accept-Ranges", "bytes");
|
|
|
|
response.setHeader("Content-Type", contentType);
|
|
|
|
response.setHeader("Content-Length", end - start);
|
|
|
|
response.setHeader(
|
|
|
|
"Content-Range",
|
|
|
|
`bytes ${start}-${end - 1}/${fileSize}`
|
|
|
|
);
|
|
|
|
response.writeHead(206);
|
|
|
|
stream.pipe(response);
|
|
|
|
}
|
2024-02-11 21:37:01 +09:00
|
|
|
}
|
2014-03-22 09:47:23 +09:00
|
|
|
|
2017-08-31 21:08:22 +09:00
|
|
|
// 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") {
|
2020-04-17 19:06:27 +09:00
|
|
|
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
|
2017-08-31 21:08:22 +09:00
|
|
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
|
|
}
|
|
|
|
if (req.url === "/test/pdfs/basicapi.pdf?cors=withoutCredentials") {
|
2020-04-17 19:06:27 +09:00
|
|
|
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
|
2017-08-31 21:08:22 +09:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-08 19:44:37 +09:00
|
|
|
export { WebServer };
|