Browse Source

create new response module & use it for avatars

jomo 10 years ago
parent
commit
3cbf73b0d7
5 changed files with 157 additions and 53 deletions
  1. 86 0
      lib/response.js
  2. 30 27
      lib/routes/avatars.js
  3. 3 2
      lib/routes/renders.js
  4. 34 23
      lib/server.js
  5. 4 1
      lib/views/index.jade

+ 86 - 0
lib/response.js

@@ -0,0 +1,86 @@
+var logging = require("./logging");
+var config = require("./config");
+var crc = require("crc").crc32;
+
+var human_status = {
+  "-2": "user error",
+  "-1": "server error",
+  0: "none",
+  1: "cached",
+  2: "downloaded",
+  3: "checked",
+};
+
+
+// handles HTTP responses
+// +request+ a http.IncomingMessage
+// +response+ a http.ServerResponse
+// +result+ an object with:
+//  * status:   see human_status, required
+//  * redirect: redirect URL
+//  * body:     file or message, required unless redirect is present or status is < 0
+//  * type:     a valid Content-Type, required if body is present
+//  * hash:     image hash, required when body is an image
+//  * err:      a possible Error
+module.exports = function(request, response, result) {
+
+  response.on("close", function() {
+    logging.warn(request.id, "Connection closed");
+  });
+
+  response.on("finish", function() {
+    logging.log(request.id, response.statusCode, "(" + human_status[result.status] + ")");
+  });
+
+  response.on("error", function(err) {
+    logging.error(request.id, err);
+  });
+
+  // These headers are the same for every response
+  var headers = {
+    "Content-Type": result.type || "text/plain",
+    "Cache-Control": "max-age=" + config.browser_cache_time + ", public",
+    "Response-Time": Date.now() - request.start,
+    "X-Storage-Type": human_status[result.status],
+    "X-Request-ID": request.id,
+    "Access-Control-Allow-Origin": "*"
+  };
+
+  if (result.err) {
+    logging.error(result.err);
+  }
+
+  if (result.body) {
+    // use Mojang's image hash if available
+    // use crc32 as a hash function otherwise
+    var etag = result.body && result.hash && result.hash.substr(0, 10) || crc(result.body);
+    headers.Etag = "\"" + etag + "\"";
+
+    // handle etag caching
+    var incoming_etag = request.headers["if-none-match"];
+    if (incoming_etag && incoming_etag === headers.Etag) {
+      logging.debug("Etag matches");
+      response.writeHead(304, headers);
+      response.end();
+      return;
+    }
+  }
+
+  if (result.redirect) {
+    headers.Location = result.redirect;
+    response.writeHead(307, headers);
+    response.end();
+    return;
+  }
+
+  if (result.status === -2) {
+    response.writeHead(422, headers);
+    response.end(result.body);
+  } else if (result.status === -1) {
+    response.writeHead(500, headers);
+    response.end(result.body);
+  } else {
+    response.writeHead(200, headers);
+    response.end(result.body);
+  }
+};

+ 30 - 27
lib/routes/avatars.js

@@ -3,22 +3,23 @@ var helpers = require("../helpers");
 var config = require("../config");
 var skins = require("../skins");
 var cache = require("../cache");
+var path = require("path");
 
-var human_status = {
-  0: "none",
-  1: "cached",
-  2: "downloaded",
-  3: "checked",
-  "-1": "error"
-};
-
-function handle_default(http_status, img_status, userId, size, def, callback) {
+function handle_default(img_status, userId, size, def, callback) {
   if (def && def !== "steve" && def !== "alex") {
-    callback(http_status, img_status, def);
+    callback({
+      status: img_status,
+      redirect: def
+    });
   } else {
     def = def || skins.default_skin(userId);
-    skins.resize_img("public/images/" + def + ".png", size, function(err, image) {
-      callback(http_status, img_status, image);
+    skins.resize_img(path.join(__dirname, "..", "public", "images", def + ".png"), size, function(err, image) {
+      callback({
+        status: img_status,
+        body: image,
+        type: "image/png",
+        err: err
+      });
     });
   }
 }
@@ -29,16 +30,21 @@ module.exports = function(req, callback) {
   var size = parseInt(req.url.query.size) || config.default_size;
   var def = req.url.query.default;
   var helm = req.url.query.hasOwnProperty("helm");
-  var etag = null;
 
   // Prevent app from crashing/freezing
   if (size < config.min_size || size > config.max_size) {
     // "Unprocessable Entity", valid request, but semantically erroneous:
     // https://tools.ietf.org/html/rfc4918#page-78
-    callback(422, 0, "Invalid Size");
+    callback({
+      status: -2,
+      body: "Invalid Size"
+    });
     return;
   } else if (!helpers.id_valid(userId)) {
-    callback(422, 0, "Invalid ID");
+    callback({
+      status: -2,
+      body: "Invalid userid"
+    });
     return;
   }
 
@@ -48,7 +54,6 @@ module.exports = function(req, callback) {
 
   try {
     helpers.get_avatar(req.id, userId, helm, size, function(err, status, image, hash) {
-      logging.log(req.id, "storage type:", human_status[status]);
       if (err) {
         logging.error(req.id, err);
         if (err.code === "ENOENT") {
@@ -56,22 +61,20 @@ module.exports = function(req, callback) {
           cache.remove_hash(req.id, userId);
         }
       }
-      etag = image && hash && hash.substr(0, 32) || "none";
-      var matches = req.headers["if-none-match"] === '"' + etag + '"';
       if (image) {
-        var http_status = 200;
-        if (err) {
-          http_status = 503;
-        }
-        logging.debug(req.id, "etag:", req.headers["if-none-match"]);
-        logging.debug(req.id, "matches:", matches);
-        callback(matches ? 304 : http_status, status, image);
+        callback({
+          status: status,
+          body: image,
+          type: "image/png",
+          err: err,
+          hash: hash
+        });
       } else {
-        handle_default(matches ? 304 : 200, status, userId, size, def, callback);
+        handle_default(status, userId, size, def, callback);
       }
     });
   } catch(e) {
     logging.error(req.id, "error:", e.stack);
-    handle_default(500, -1, userId, size, def, callback);
+    handle_default(-1, userId, size, def, callback);
   }
 };

+ 3 - 2
lib/routes/renders.js

@@ -1,9 +1,10 @@
 var logging = require("../logging");
 var helpers = require("../helpers");
+var renders = require("../renders");
 var config = require("../config");
 var cache = require("../cache");
 var skins = require("../skins");
-var renders = require("../renders");
+var path = require("path");
 var fs = require("fs");
 
 var human_status = {
@@ -71,7 +72,7 @@ module.exports = function(req, res) {
       res.end();
     } else {
       def = def || skins.default_skin(userId);
-      fs.readFile("public/images/" + def + "_skin.png", function (err, buf) {
+      fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function (err, buf) {
         if (err) {
           // errored while loading the default image, continuing with null image
           logging.error(rid, "error loading default render image:", err);

+ 34 - 23
lib/server.js

@@ -1,6 +1,7 @@
 #!/usr/bin/env node
 var logging = require("./logging");
 var querystring = require("querystring");
+var response = require("./response");
 var config = require("./config");
 var http = require("http");
 var mime = require("mime");
@@ -35,53 +36,63 @@ function asset_request(req, res) {
   });
 }
 
-function requestHandler(req, res) {
-  var request = req;
-  request.url = url.parse(req.url, true);
-  request.url.query = request.url.query || {};
+// generates a 12 character random string
+function request_id() {
+  return Math.random().toString(36).substring(2, 14);
+}
 
+// splits a URL path into an Array
+// the path is resolved and decoded
+function path_list(pathname) {
   // remove trailing and double slashes + other junk
-  var path_list = request.url.pathname.split("/");
-  for (var i = 0; i < path_list.length; i++) {
+
+  // FIXME: also accepts relative paths?
+
+  pathname = path.resolve(pathname);
+  var list = pathname.split("/");
+  for (var i = 0; i < list.length; i++) {
     // URL decode
-    path_list[i] = querystring.unescape(path_list[i]);
+    list[i] = querystring.unescape(list[i]);
   }
-  request.url.path_list = path_list;
+  return list;
+}
 
-  // generate 12 character random string
-  request.id = Math.random().toString(36).substring(2, 14);
+function requestHandler(req, res) {
+  req.url = url.parse(req.url, true);
+  req.url.query = req.url.query || {};
+  req.url.path_list = path_list(req.url.pathname);
 
-  res.start = new Date();
+  req.id = request_id();
+  req.start = Date.now();
 
-  var local_path = request.url.path_list[1];
-  logging.log(request.id, request.method, request.url.href);
-  if (request.method === "GET" || request.method === "HEAD") {
+  var local_path = req.url.path_list[1];
+  logging.log(req.id, req.method, req.url.href);
+  if (req.method === "GET" || req.method === "HEAD") {
     try {
       switch (local_path) {
         case "":
-        routes.index(request, res);
+        routes.index(req, res);
         break;
         case "avatars":
-        routes.avatars(request, function(http_status, img_status, body) {
-          res.writeHead(http_status, {});
-          res.end(body);
+        routes.avatars(req, function(result) {
+          response(req, res, result);
         });
         break;
         case "skins":
-        routes.skins(request, res);
+        routes.skins(req, res);
         break;
         case "renders":
-        routes.renders(request, res);
+        routes.renders(req, res);
         break;
         case "capes":
-        routes.capes(request, res);
+        routes.capes(req, res);
         break;
         default:
-        asset_request(request, res);
+        asset_request(req, res);
       }
     } catch(e) {
       var error = JSON.stringify(req.headers) + "\n" + e.stack;
-      logging.error(request.id + "Error:", error);
+      logging.error(req.id + "Error:", error);
       res.writeHead(500, {
         "Content-Type": "text/plain"
       });

+ 4 - 1
lib/views/index.jade

@@ -287,8 +287,11 @@ block content
                   | This happens either when the user removed their skin or when it didn't change.
                 li <b>downloaded</b>: 2 external requests. Skin changed or unknown, downloaded.
                 li
-                  | <b>error</b>: This can happen, for example, when Mojang's servers are down.<br>
+                  | <b>server error</b>: This can happen, for example, when Mojang's servers are down.<br>
                   | If possible, an outdated image is served instead.
+                li
+                  | <b>user error</b>: You have done something wrong, such as requesting a malformed userid.<br>
+                  | Check the response body for details.
             section
               a(id="meta-x-request-id" class="anchor")
               a(href="#meta-x-request-id")