webserver.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. /*
  2. * Copyright 2014 Mozilla Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. /* eslint-disable no-var */
  17. "use strict";
  18. var http = require("http");
  19. var path = require("path");
  20. var fs = require("fs");
  21. var mimeTypes = {
  22. ".css": "text/css",
  23. ".html": "text/html",
  24. ".js": "application/javascript",
  25. ".json": "application/json",
  26. ".svg": "image/svg+xml",
  27. ".pdf": "application/pdf",
  28. ".xhtml": "application/xhtml+xml",
  29. ".gif": "image/gif",
  30. ".ico": "image/x-icon",
  31. ".png": "image/png",
  32. ".log": "text/plain",
  33. ".bcmap": "application/octet-stream",
  34. ".properties": "text/plain",
  35. };
  36. var defaultMimeType = "application/octet-stream";
  37. function WebServer() {
  38. this.root = ".";
  39. this.host = "localhost";
  40. this.port = 0;
  41. this.server = null;
  42. this.verbose = false;
  43. this.cacheExpirationTime = 0;
  44. this.disableRangeRequests = false;
  45. this.hooks = {
  46. GET: [crossOriginHandler],
  47. POST: [],
  48. };
  49. }
  50. WebServer.prototype = {
  51. start(callback) {
  52. this._ensureNonZeroPort();
  53. this.server = http.createServer(this._handler.bind(this));
  54. this.server.listen(this.port, this.host, callback);
  55. console.log(
  56. "Server running at http://" + this.host + ":" + this.port + "/"
  57. );
  58. },
  59. stop(callback) {
  60. this.server.close(callback);
  61. this.server = null;
  62. },
  63. _ensureNonZeroPort() {
  64. if (!this.port) {
  65. // If port is 0, a random port will be chosen instead. Do not set a host
  66. // name to make sure that the port is synchronously set by .listen().
  67. var server = http.createServer().listen(0);
  68. var address = server.address();
  69. // .address().port being available synchronously is merely an
  70. // implementation detail. So we are defensive here and fall back to some
  71. // fixed port when the address is not available yet.
  72. this.port = address ? address.port : 8000;
  73. server.close();
  74. }
  75. },
  76. _handler(req, res) {
  77. var url = req.url.replace(/\/\//g, "/");
  78. var urlParts = /([^?]*)((?:\?(.*))?)/.exec(url);
  79. try {
  80. // Guard against directory traversal attacks such as
  81. // `/../../../../../../../etc/passwd`, which let you make GET requests
  82. // for files outside of `this.root`.
  83. var pathPart = path.normalize(decodeURI(urlParts[1]));
  84. // path.normalize returns a path on the basis of the current platform.
  85. // Windows paths cause issues in statFile and serverDirectoryIndex.
  86. // Converting to unix path would avoid platform checks in said functions.
  87. pathPart = pathPart.replace(/\\/g, "/");
  88. } catch (ex) {
  89. // If the URI cannot be decoded, a `URIError` is thrown. This happens for
  90. // malformed URIs such as `http://localhost:8888/%s%s` and should be
  91. // handled as a bad request.
  92. res.writeHead(400);
  93. res.end("Bad request", "utf8");
  94. return;
  95. }
  96. var queryPart = urlParts[3];
  97. var verbose = this.verbose;
  98. var methodHooks = this.hooks[req.method];
  99. if (!methodHooks) {
  100. res.writeHead(405);
  101. res.end("Unsupported request method", "utf8");
  102. return;
  103. }
  104. var handled = methodHooks.some(function (hook) {
  105. return hook(req, res);
  106. });
  107. if (handled) {
  108. return;
  109. }
  110. if (pathPart === "/favicon.ico") {
  111. fs.realpath(
  112. path.join(this.root, "test/resources/favicon.ico"),
  113. checkFile
  114. );
  115. return;
  116. }
  117. var disableRangeRequests = this.disableRangeRequests;
  118. var cacheExpirationTime = this.cacheExpirationTime;
  119. var filePath;
  120. fs.realpath(path.join(this.root, pathPart), checkFile);
  121. function checkFile(err, file) {
  122. if (err) {
  123. res.writeHead(404);
  124. res.end();
  125. if (verbose) {
  126. console.error(url + ": not found");
  127. }
  128. return;
  129. }
  130. filePath = file;
  131. fs.stat(filePath, statFile);
  132. }
  133. var fileSize;
  134. function statFile(err, stats) {
  135. if (err) {
  136. res.writeHead(500);
  137. res.end();
  138. return;
  139. }
  140. fileSize = stats.size;
  141. var isDir = stats.isDirectory();
  142. if (isDir && !/\/$/.test(pathPart)) {
  143. res.setHeader("Location", pathPart + "/" + urlParts[2]);
  144. res.writeHead(301);
  145. res.end("Redirected", "utf8");
  146. return;
  147. }
  148. if (isDir) {
  149. serveDirectoryIndex(filePath);
  150. return;
  151. }
  152. var range = req.headers.range;
  153. if (range && !disableRangeRequests) {
  154. var rangesMatches = /^bytes=(\d+)-(\d+)?/.exec(range);
  155. if (!rangesMatches) {
  156. res.writeHead(501);
  157. res.end("Bad range", "utf8");
  158. if (verbose) {
  159. console.error(url + ': bad range: "' + range + '"');
  160. }
  161. return;
  162. }
  163. var start = +rangesMatches[1];
  164. var end = +rangesMatches[2];
  165. if (verbose) {
  166. console.log(url + ": range " + start + " - " + end);
  167. }
  168. serveRequestedFileRange(
  169. filePath,
  170. start,
  171. isNaN(end) ? fileSize : end + 1
  172. );
  173. return;
  174. }
  175. if (verbose) {
  176. console.log(url);
  177. }
  178. serveRequestedFile(filePath);
  179. }
  180. function escapeHTML(untrusted) {
  181. // Escape untrusted input so that it can safely be used in a HTML response
  182. // in HTML and in HTML attributes.
  183. return untrusted
  184. .replace(/&/g, "&")
  185. .replace(/</g, "&lt;")
  186. .replace(/>/g, "&gt;")
  187. .replace(/"/g, "&quot;")
  188. .replace(/'/g, "&#39;");
  189. }
  190. function serveDirectoryIndex(dir) {
  191. res.setHeader("Content-Type", "text/html");
  192. res.writeHead(200);
  193. if (queryPart === "frame") {
  194. res.end(
  195. "<html><frameset cols=*,200><frame name=pdf>" +
  196. '<frame src="' +
  197. encodeURI(pathPart) +
  198. '?side"></frameset></html>',
  199. "utf8"
  200. );
  201. return;
  202. }
  203. var all = queryPart === "all";
  204. fs.readdir(dir, function (err, files) {
  205. if (err) {
  206. res.end();
  207. return;
  208. }
  209. res.write(
  210. '<html><head><meta charset="utf-8"></head><body>' +
  211. "<h1>PDFs of " +
  212. pathPart +
  213. "</h1>\n"
  214. );
  215. if (pathPart !== "/") {
  216. res.write('<a href="..">..</a><br>\n');
  217. }
  218. files.forEach(function (file) {
  219. var stat;
  220. var item = pathPart + file;
  221. var href = "";
  222. var label = "";
  223. var extraAttributes = "";
  224. try {
  225. stat = fs.statSync(path.join(dir, file));
  226. } catch (e) {
  227. href = encodeURI(item);
  228. label = file + " (" + e + ")";
  229. extraAttributes = ' style="color:red"';
  230. }
  231. if (stat) {
  232. if (stat.isDirectory()) {
  233. href = encodeURI(item);
  234. label = file;
  235. } else if (path.extname(file).toLowerCase() === ".pdf") {
  236. href = "/web/viewer.html?file=" + encodeURIComponent(item);
  237. label = file;
  238. extraAttributes = ' target="pdf"';
  239. } else if (all) {
  240. href = encodeURI(item);
  241. label = file;
  242. }
  243. }
  244. if (label) {
  245. res.write(
  246. '<a href="' +
  247. escapeHTML(href) +
  248. '"' +
  249. extraAttributes +
  250. ">" +
  251. escapeHTML(label) +
  252. "</a><br>\n"
  253. );
  254. }
  255. });
  256. if (files.length === 0) {
  257. res.write("<p>no files found</p>\n");
  258. }
  259. if (!all && queryPart !== "side") {
  260. res.write(
  261. "<hr><p>(only PDF files are shown, " +
  262. '<a href="?all">show all</a>)</p>\n'
  263. );
  264. }
  265. res.end("</body></html>");
  266. });
  267. }
  268. function serveRequestedFile(reqFilePath) {
  269. var stream = fs.createReadStream(reqFilePath, { flags: "rs" });
  270. stream.on("error", function (error) {
  271. res.writeHead(500);
  272. res.end();
  273. });
  274. var ext = path.extname(reqFilePath).toLowerCase();
  275. var contentType = mimeTypes[ext] || defaultMimeType;
  276. if (!disableRangeRequests) {
  277. res.setHeader("Accept-Ranges", "bytes");
  278. }
  279. res.setHeader("Content-Type", contentType);
  280. res.setHeader("Content-Length", fileSize);
  281. if (cacheExpirationTime > 0) {
  282. var expireTime = new Date();
  283. expireTime.setSeconds(expireTime.getSeconds() + cacheExpirationTime);
  284. res.setHeader("Expires", expireTime.toUTCString());
  285. }
  286. res.writeHead(200);
  287. stream.pipe(res);
  288. }
  289. function serveRequestedFileRange(reqFilePath, start, end) {
  290. var stream = fs.createReadStream(reqFilePath, {
  291. flags: "rs",
  292. start,
  293. end: end - 1,
  294. });
  295. stream.on("error", function (error) {
  296. res.writeHead(500);
  297. res.end();
  298. });
  299. var ext = path.extname(reqFilePath).toLowerCase();
  300. var contentType = mimeTypes[ext] || defaultMimeType;
  301. res.setHeader("Accept-Ranges", "bytes");
  302. res.setHeader("Content-Type", contentType);
  303. res.setHeader("Content-Length", end - start);
  304. res.setHeader(
  305. "Content-Range",
  306. "bytes " + start + "-" + (end - 1) + "/" + fileSize
  307. );
  308. res.writeHead(206);
  309. stream.pipe(res);
  310. }
  311. },
  312. };
  313. // This supports the "Cross-origin" test in test/unit/api_spec.js
  314. // It is here instead of test.js so that when the test will still complete as
  315. // expected if the user does "gulp server" and then visits
  316. // http://localhost:8888/test/unit/unit_test.html?spec=Cross-origin
  317. function crossOriginHandler(req, res) {
  318. if (req.url === "/test/pdfs/basicapi.pdf?cors=withCredentials") {
  319. res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  320. res.setHeader("Access-Control-Allow-Credentials", "true");
  321. }
  322. if (req.url === "/test/pdfs/basicapi.pdf?cors=withoutCredentials") {
  323. res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
  324. }
  325. }
  326. exports.WebServer = WebServer;