create new response module & use it for avatars

This commit is contained in:
jomo 2015-04-20 00:41:11 +02:00
parent fce58722c8
commit 3cbf73b0d7
5 changed files with 157 additions and 53 deletions

86
lib/response.js Normal file
View File

@ -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);
}
};

View File

@ -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);
}
};

View File

@ -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);

View File

@ -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"
});

View File

@ -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")