server.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #!/usr/bin/env node
  2. var util = require("util"),
  3. http = require("http"),
  4. fs = require("fs"),
  5. url = require("url"),
  6. events = require("events");
  7. var DEFAULT_PORT = 3001;
  8. function main(argv) {
  9. new HttpServer({
  10. GET: createServlet(StaticServlet),
  11. HEAD: createServlet(StaticServlet)
  12. }).start(Number(argv[2]) || DEFAULT_PORT);
  13. }
  14. function escapeHtml(value) {
  15. return value
  16. .toString()
  17. .replace("<", "&lt;")
  18. .replace(">", "&gt;")
  19. .replace('"', "&quot;");
  20. }
  21. function createServlet(Class) {
  22. var servlet = new Class();
  23. return servlet.handleRequest.bind(servlet);
  24. }
  25. /**
  26. * An Http server implementation that uses a map of methods to decide
  27. * action routing.
  28. *
  29. * @param {Object} Map of method => Handler function
  30. */
  31. function HttpServer(handlers) {
  32. this.handlers = handlers;
  33. this.server = http.createServer(this.handleRequest_.bind(this));
  34. }
  35. HttpServer.prototype.start = function(port) {
  36. this.port = port;
  37. this.server.listen(port);
  38. util.puts("Http Server running at http://localhost:" + port + "/");
  39. };
  40. HttpServer.prototype.parseUrl_ = function(urlString) {
  41. var parsed = url.parse(urlString);
  42. parsed.pathname = url.resolve("/", parsed.pathname);
  43. return url.parse(url.format(parsed), true);
  44. };
  45. HttpServer.prototype.handleRequest_ = function(req, res) {
  46. var logEntry = req.method + " " + req.url;
  47. if (req.headers["user-agent"]) {
  48. logEntry += " " + req.headers["user-agent"];
  49. }
  50. util.puts(logEntry);
  51. req.url = this.parseUrl_(req.url);
  52. var handler = this.handlers[req.method];
  53. if (!handler) {
  54. res.writeHead(501);
  55. res.end();
  56. } else {
  57. handler.call(this, req, res);
  58. }
  59. };
  60. /**
  61. * Handles static content.
  62. */
  63. function StaticServlet() {}
  64. StaticServlet.MimeMap = {
  65. txt: "text/plain",
  66. html: "text/html",
  67. css: "text/css",
  68. xml: "application/xml",
  69. json: "application/json",
  70. js: "application/javascript",
  71. jpg: "image/jpeg",
  72. jpeg: "image/jpeg",
  73. gif: "image/gif",
  74. png: "image/png",
  75. svg: "image/svg+xml"
  76. };
  77. StaticServlet.prototype.handleRequest = function(req, res) {
  78. var that = this;
  79. var path = ("./" + req.url.pathname)
  80. .replace("//", "/")
  81. .replace(/%(..)/g, function(match, hex) {
  82. return String.fromCharCode(parseInt(hex, 16));
  83. });
  84. var parts = path.split("/");
  85. if (parts[parts.length - 1].charAt(0) === ".")
  86. return that.sendForbidden_(req, res, path);
  87. fs.stat(path, function(err, stat) {
  88. if (err) return that.sendMissing_(req, res, path);
  89. if (stat.isDirectory()) return that.sendDirectory_(req, res, path);
  90. return that.sendFile_(req, res, path);
  91. });
  92. };
  93. StaticServlet.prototype.sendError_ = function(req, res, error) {
  94. res.writeHead(500, {
  95. "Content-Type": "text/html"
  96. });
  97. res.write("<!doctype html>\n");
  98. res.write("<title>Internal Server Error</title>\n");
  99. res.write("<h1>Internal Server Error</h1>");
  100. res.write("<pre>" + escapeHtml(util.inspect(error)) + "</pre>");
  101. util.puts("500 Internal Server Error");
  102. util.puts(util.inspect(error));
  103. };
  104. StaticServlet.prototype.sendMissing_ = function(req, res, path) {
  105. path = path.substring(1);
  106. res.writeHead(404, {
  107. "Content-Type": "text/html"
  108. });
  109. res.write("<!doctype html>\n");
  110. res.write("<title>404 Not Found</title>\n");
  111. res.write("<h1>Not Found</h1>");
  112. res.write(
  113. "<p>The requested URL " +
  114. escapeHtml(path) +
  115. " was not found on this server.</p>"
  116. );
  117. res.end();
  118. util.puts("404 Not Found: " + path);
  119. };
  120. StaticServlet.prototype.sendForbidden_ = function(req, res, path) {
  121. path = path.substring(1);
  122. res.writeHead(403, {
  123. "Content-Type": "text/html"
  124. });
  125. res.write("<!doctype html>\n");
  126. res.write("<title>403 Forbidden</title>\n");
  127. res.write("<h1>Forbidden</h1>");
  128. res.write(
  129. "<p>You do not have permission to access " +
  130. escapeHtml(path) +
  131. " on this server.</p>"
  132. );
  133. res.end();
  134. util.puts("403 Forbidden: " + path);
  135. };
  136. StaticServlet.prototype.sendRedirect_ = function(req, res, redirectUrl) {
  137. res.writeHead(301, {
  138. "Content-Type": "text/html",
  139. Location: redirectUrl
  140. });
  141. res.write("<!doctype html>\n");
  142. res.write("<title>301 Moved Permanently</title>\n");
  143. res.write("<h1>Moved Permanently</h1>");
  144. res.write(
  145. '<p>The document has moved <a href="' + redirectUrl + '">here</a>.</p>'
  146. );
  147. res.end();
  148. util.puts("301 Moved Permanently: " + redirectUrl);
  149. };
  150. StaticServlet.prototype.sendFile_ = function(req, res, path) {
  151. var that = this;
  152. var file = fs.createReadStream(path);
  153. res.writeHead(200, {
  154. "Content-Type": StaticServlet.MimeMap[path.split(".").pop()] || "text/plain"
  155. });
  156. if (req.method === "HEAD") {
  157. res.end();
  158. } else {
  159. file.on("data", res.write.bind(res));
  160. file.on("close", function() {
  161. res.end();
  162. });
  163. file.on("error", function(error) {
  164. that.sendError_(req, res, error);
  165. });
  166. }
  167. };
  168. StaticServlet.prototype.sendDirectory_ = function(req, res, path) {
  169. var that = this;
  170. if (path.match(/[^\/]$/)) {
  171. req.url.pathname += "/";
  172. var redirectUrl = url.format(url.parse(url.format(req.url)));
  173. return that.sendRedirect_(req, res, redirectUrl);
  174. }
  175. fs.readdir(path, function(err, files) {
  176. if (err) return that.sendError_(req, res, error);
  177. if (!files.length) return that.writeDirectoryIndex_(req, res, path, []);
  178. var remaining = files.length;
  179. files.forEach(function(fileName, index) {
  180. fs.stat(path + "/" + fileName, function(err, stat) {
  181. if (err) return that.sendError_(req, res, err);
  182. if (stat.isDirectory()) {
  183. files[index] = fileName + "/";
  184. }
  185. if (!--remaining)
  186. return that.writeDirectoryIndex_(req, res, path, files);
  187. });
  188. });
  189. });
  190. };
  191. StaticServlet.prototype.writeDirectoryIndex_ = function(req, res, path, files) {
  192. path = path.substring(1);
  193. res.writeHead(200, {
  194. "Content-Type": "text/html"
  195. });
  196. if (req.method === "HEAD") {
  197. res.end();
  198. return;
  199. }
  200. res.write("<!doctype html>\n");
  201. res.write("<title>" + escapeHtml(path) + "</title>\n");
  202. res.write("<style>\n");
  203. res.write(" ol { list-style-type: none; font-size: 1.2em; }\n");
  204. res.write("</style>\n");
  205. res.write("<h1>Directory: " + escapeHtml(path) + "</h1>");
  206. res.write("<ol>");
  207. files.forEach(function(fileName) {
  208. if (fileName.charAt(0) !== ".") {
  209. res.write(
  210. '<li><a href="' +
  211. escapeHtml(fileName) +
  212. '">' +
  213. escapeHtml(fileName) +
  214. "</a></li>"
  215. );
  216. }
  217. });
  218. res.write("</ol>");
  219. res.end();
  220. };
  221. // Must be last,
  222. main(process.argv);