From 3cbf73b0d751e32ef190afb6fba1dbc3ef071c7f Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 20 Apr 2015 00:41:11 +0200 Subject: [PATCH] create new response module & use it for avatars --- lib/response.js | 86 +++++++++++++++++++++++++++++++++++++++++++ lib/routes/avatars.js | 57 ++++++++++++++-------------- lib/routes/renders.js | 5 ++- lib/server.js | 57 ++++++++++++++++------------ lib/views/index.jade | 5 ++- 5 files changed, 157 insertions(+), 53 deletions(-) create mode 100644 lib/response.js diff --git a/lib/response.js b/lib/response.js new file mode 100644 index 0000000..8344642 --- /dev/null +++ b/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); + } +}; \ No newline at end of file diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index 21b5ec2..36bf6dd 100644 --- a/lib/routes/avatars.js +++ b/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); } }; \ No newline at end of file diff --git a/lib/routes/renders.js b/lib/routes/renders.js index 7c2853d..8d0b6ba 100644 --- a/lib/routes/renders.js +++ b/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); diff --git a/lib/server.js b/lib/server.js index d414b25..732b785 100644 --- a/lib/server.js +++ b/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" }); diff --git a/lib/views/index.jade b/lib/views/index.jade index 65db2aa..4e2359a 100644 --- a/lib/views/index.jade +++ b/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 downloaded: 2 external requests. Skin changed or unknown, downloaded. li - | error: This can happen, for example, when Mojang's servers are down.
+ | server error: This can happen, for example, when Mojang's servers are down.
| If possible, an outdated image is served instead. + li + | user error: You have done something wrong, such as requesting a malformed userid.
+ | Check the response body for details. section a(id="meta-x-request-id" class="anchor") a(href="#meta-x-request-id")