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