Merge branch 'http-response'

This commit is contained in:
jomo 2015-05-06 22:36:22 +02:00
commit 9c59d302c9
13 changed files with 838 additions and 486 deletions

View File

@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- "iojs-v1.6.0" - iojs-v2.0
before_install: before_install:
- sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++ - sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev libgif-dev build-essential g++
script: script:
@ -8,7 +8,7 @@ script:
notifications: notifications:
irc: irc:
channels: channels:
- "irc.esper.net#crafatar" - irc.esper.net#crafatar
skip_join: true skip_join: true
services: services:
- redis-server - redis-server

View File

@ -98,7 +98,7 @@ exp.info = function(callback) {
// these 60 seconds match the duration of Mojang's rate limit ban // these 60 seconds match the duration of Mojang's rate limit ban
// callback: error // callback: error
exp.update_timestamp = function(rid, userId, hash, temp, callback) { exp.update_timestamp = function(rid, userId, hash, temp, callback) {
logging.log(rid, "cache: updating timestamp"); logging.debug(rid, "updating cache timestamp");
var sub = temp ? (config.local_cache_time - 60) : 0; var sub = temp ? (config.local_cache_time - 60) : 0;
var time = Date.now() - sub; var time = Date.now() - sub;
// store userId in lower case if not null // store userId in lower case if not null
@ -114,7 +114,7 @@ exp.update_timestamp = function(rid, userId, hash, temp, callback) {
// this feature can be used to write both cape and skin at separate times // this feature can be used to write both cape and skin at separate times
// +callback+ contans error // +callback+ contans error
exp.save_hash = function(rid, userId, skin_hash, cape_hash, callback) { exp.save_hash = function(rid, userId, skin_hash, cape_hash, callback) {
logging.log(rid, "cache: saving skin:" + skin_hash + " cape:" + cape_hash); logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash);
var time = Date.now(); var time = Date.now();
// store shorter null byte instead of "null" // store shorter null byte instead of "null"
skin_hash = (skin_hash === null ? "" : skin_hash); skin_hash = (skin_hash === null ? "" : skin_hash);
@ -138,7 +138,7 @@ exp.save_hash = function(rid, userId, skin_hash, cape_hash, callback) {
// removes the hash for +userId+ from the cache // removes the hash for +userId+ from the cache
exp.remove_hash = function(rid, userId) { exp.remove_hash = function(rid, userId) {
logging.log(rid, "cache: deleting hash"); logging.log(rid, "deleting hash from cache");
redis.del(userId.toLowerCase(), "h", "t"); redis.del(userId.toLowerCase(), "h", "t");
}; };

View File

@ -177,7 +177,7 @@ function store_images(rid, userId, cache_details, type, callback) {
// an error occured, not caching. we can try in 60 seconds // an error occured, not caching. we can try in 60 seconds
callback_for(userId, "skin", store_err, null); callback_for(userId, "skin", store_err, null);
} else { } else {
cache.save_hash(rid, userId, skin_hash, null, function(cache_err) { cache.save_hash(rid, userId, skin_hash, undefined, function(cache_err) {
callback_for(userId, "skin", (store_err || cache_err), skin_hash); callback_for(userId, "skin", (store_err || cache_err), skin_hash);
}); });
} }
@ -210,12 +210,7 @@ exp.id_valid = function(userId) {
// decides whether to get a +type+ image for +userId+ from disk or to download it // decides whether to get a +type+ image for +userId+ from disk or to download it
// callback: error, status, hash // callback: error, status, hash
// the status gives information about how the image was received // for status, see response.js
// -1: "error"
// 0: "none" - cached as null
// 1: "cached" - found on disk
// 2: "downloaded" - profile downloaded, skin downloaded from mojang servers
// 3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
exp.get_image_hash = function(rid, userId, type, callback) { exp.get_image_hash = function(rid, userId, type, callback) {
cache.get_details(userId, function(err, cache_details) { cache.get_details(userId, function(err, cache_details) {
var cached_hash = null; var cached_hash = null;
@ -286,23 +281,26 @@ exp.get_avatar = function(rid, userId, helm, size, callback) {
}; };
// handles requests for +userId+ skins // handles requests for +userId+ skins
// callback: error, skin hash, image buffer // callback: error, skin hash, status, image buffer
exp.get_skin = function(rid, userId, callback) { exp.get_skin = function(rid, userId, callback) {
exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
// FIXME: err is not used / handled if (skin_hash) {
var skinpath = path.join(__dirname, "..", config.skins_dir, skin_hash + ".png"); var skinpath = path.join(__dirname, "..", config.skins_dir, skin_hash + ".png");
fs.exists(skinpath, function(exists) { fs.exists(skinpath, function(exists) {
if (exists) { if (exists) {
logging.log(rid, "skin already exists, not downloading"); logging.log(rid, "skin already exists, not downloading");
skins.open_skin(rid, skinpath, function(skin_err, img) { skins.open_skin(rid, skinpath, function(skin_err, img) {
callback(skin_err, skin_hash, img); callback(skin_err || err, skin_hash, status, img);
}); });
} else { } else {
networking.save_texture(rid, skin_hash, skinpath, function(net_err, response, img) { networking.save_texture(rid, skin_hash, skinpath, function(net_err, response, img) {
callback(net_err, skin_hash, img); callback(net_err || err, skin_hash, status, img);
}); });
} }
}); });
} else {
callback(err, null, status, null);
}
}); });
}; };
@ -317,9 +315,9 @@ function get_type(helm, body) {
// handles creations of 3D renders // handles creations of 3D renders
// callback: error, skin hash, image buffer // callback: error, skin hash, image buffer
exp.get_render = function(rid, userId, scale, helm, body, callback) { exp.get_render = function(rid, userId, scale, helm, body, callback) {
exp.get_skin(rid, userId, function(err, skin_hash, img) { exp.get_skin(rid, userId, function(err, skin_hash, status, img) {
if (!skin_hash) { if (!skin_hash) {
callback(err, -1, skin_hash, null); callback(err, status, skin_hash, null);
return; return;
} }
var renderpath = path.join(__dirname, "..", config.renders_dir, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); var renderpath = path.join(__dirname, "..", config.renders_dir, [skin_hash, scale, get_type(helm, body)].join("-") + ".png");
@ -344,7 +342,7 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) {
if (fs_err) { if (fs_err) {
logging.error(rid, fs_err.stack); logging.error(rid, fs_err.stack);
} }
callback(null, 2, skin_hash, img); callback(null, 2, skin_hash, drawn_img);
}); });
} }
}); });
@ -354,11 +352,11 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) {
}; };
// handles requests for +userId+ capes // handles requests for +userId+ capes
// callback: error, cape hash, image buffer // callback: error, cape hash, status, image buffer
exp.get_cape = function(rid, userId, callback) { exp.get_cape = function(rid, userId, callback) {
exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash) { exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash) {
if (!cape_hash) { if (!cape_hash) {
callback(err, null, null); callback(err, null, status, null);
return; return;
} }
var capepath = path.join(__dirname, "..", config.capes_dir, cape_hash + ".png"); var capepath = path.join(__dirname, "..", config.capes_dir, cape_hash + ".png");
@ -366,14 +364,14 @@ exp.get_cape = function(rid, userId, callback) {
if (exists) { if (exists) {
logging.log(rid, "cape already exists, not downloading"); logging.log(rid, "cape already exists, not downloading");
skins.open_skin(rid, capepath, function(skin_err, img) { skins.open_skin(rid, capepath, function(skin_err, img) {
callback(skin_err, cape_hash, img); callback(skin_err || err, cape_hash, status, img);
}); });
} else { } else {
networking.save_texture(rid, cape_hash, capepath, function(net_err, response, img) { networking.save_texture(rid, cape_hash, capepath, function(net_err, response, img) {
if (response && response.statusCode === 404) { if (response && response.statusCode === 404) {
callback(net_err, cape_hash, null); callback(net_err, cape_hash, status, null);
} else { } else {
callback(net_err, cape_hash, img); callback(net_err, cape_hash, status, img);
} }
}); });
} }

90
lib/response.js Normal file
View File

@ -0,0 +1,90 @@
var logging = require("./logging");
var config = require("./config");
var crc = require("crc").crc32;
var human_status = {
"-2": "user error", // e.g. invalid size
"-1": "server error", // e.g. network issues
0: "none", // cached as null (user has no skin)
1: "cached", // found on disk
2: "downloaded", // profile downloaded, skin downloaded from mojang servers
3: "checked", // profile re-downloaded (was too old), has no skin or skin cached
};
// handles HTTP responses
// +request+ a http.IncomingMessage
// +response+ a http.ServerResponse
// +result+ an object with:
// * status: see human_status, required for images without err
// * redirect: redirect URL
// * body: file or message, required unless redirect is present or status is < 0
// * type: a valid Content-Type for the body, defaults to "text/plain"
// * 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.body && result.type) || "text/plain",
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": Date.now() - request.start,
"X-Request-ID": request.id,
"Access-Control-Allow-Origin": "*"
};
if (result.err) {
logging.error(result.err);
logging.error(result.err.stack);
result.status = -1;
}
if (result.status !== undefined && result.status !== null) {
headers["X-Storage-Type"] = human_status[result.status];
}
if (result.body) {
// use Mojang's image hash if available
// use crc32 as a hash function otherwise
var etag = 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) {
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(result.body ? 200 : 404, headers);
response.end(result.body);
}
};

View File

@ -3,108 +3,79 @@ var helpers = require("../helpers");
var config = require("../config"); var config = require("../config");
var skins = require("../skins"); var skins = require("../skins");
var cache = require("../cache"); var cache = require("../cache");
var path = require("path");
var human_status = { function handle_default(img_status, userId, size, def, err, callback) {
0: "none", if (def && def !== "steve" && def !== "alex") {
1: "cached", callback({
2: "downloaded", status: img_status,
3: "checked", redirect: def,
"-1": "error" err: err
}; });
} else {
def = def || skins.default_skin(userId);
skins.resize_img(path.join(__dirname, "..", "public", "images", def + ".png"), size, function(resize_err, image) {
callback({
status: img_status,
body: image,
type: "image/png",
hash: def,
err: resize_err || err
});
});
}
}
// GET avatar request // GET avatar request
module.exports = function(req, res) { module.exports = function(req, callback) {
var start = new Date(); var userId = (req.url.path_list[1] || "").split(".")[0];
var userId = (req.url.path_list[2] || "").split(".")[0];
var size = parseInt(req.url.query.size) || config.default_size; var size = parseInt(req.url.query.size) || config.default_size;
var def = req.url.query.default; var def = req.url.query.default;
var helm = req.url.query.hasOwnProperty("helm"); var helm = req.url.query.hasOwnProperty("helm");
var etag = null;
var rid = req.id;
function sendimage(rid, http_status, img_status, image) {
logging.log(rid, "status:", http_status);
res.writeHead(http_status, {
"Content-Type": "image/png",
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start,
"X-Storage-Type": human_status[img_status],
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"'
});
res.end(http_status === 304 ? null : image);
}
function handle_default(rid, http_status, img_status, userId) {
if (def && def !== "steve" && def !== "alex") {
logging.log(rid, "status: 301");
res.writeHead(301, {
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start,
"X-Storage-Type": human_status[img_status],
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Location": def
});
res.end();
} else {
def = def || skins.default_skin(userId);
skins.resize_img("public/images/" + def + ".png", size, function(err, image) {
sendimage(rid, http_status, img_status, image);
});
}
}
// Prevent app from crashing/freezing // Prevent app from crashing/freezing
if (size < config.min_size || size > config.max_size) { if (size < config.min_size || size > config.max_size) {
// "Unprocessable Entity", valid request, but semantically erroneous: // "Unprocessable Entity", valid request, but semantically erroneous:
// https://tools.ietf.org/html/rfc4918#page-78 // https://tools.ietf.org/html/rfc4918#page-78
res.writeHead(422, { callback({
"Content-Type": "text/plain", status: -2,
"Response-Time": new Date() - start body: "Invalid Size"
}); });
res.end("Invalid Size");
return; return;
} else if (!helpers.id_valid(userId)) { } else if (!helpers.id_valid(userId)) {
res.writeHead(422, { callback({
"Content-Type": "text/plain", status: -2,
"Response-Time": new Date() - start body: "Invalid UserID"
}); });
res.end("Invalid ID");
return; return;
} }
// strip dashes // strip dashes
userId = userId.replace(/-/g, ""); userId = userId.replace(/-/g, "");
logging.debug(rid, "userid:", userId);
try { try {
helpers.get_avatar(rid, userId, helm, size, function(err, status, image, hash) { helpers.get_avatar(req.id, userId, helm, size, function(err, status, image, hash) {
logging.log(rid, "storage type:", human_status[status]);
if (err) { if (err) {
logging.error(rid, err); logging.error(req.id, err);
if (err.code === "ENOENT") { if (err.code === "ENOENT") {
// no such file // no such file
cache.remove_hash(rid, userId); cache.remove_hash(req.id, userId);
} }
} }
etag = image && hash && hash.substr(0, 32) || "none";
var matches = req.headers["if-none-match"] === '"' + etag + '"';
if (image) { if (image) {
var http_status = 200; callback({
if (err) { status: status,
http_status = 503; body: image,
} type: "image/png",
logging.debug(rid, "etag:", req.headers["if-none-match"]); err: err,
logging.debug(rid, "matches:", matches); hash: hash
sendimage(rid, matches ? 304 : http_status, status, image); });
} else { } else {
handle_default(rid, matches ? 304 : 200, status, userId); handle_default(status, userId, size, def, err, callback);
} }
}); });
} catch(e) { } catch(e) {
logging.error(rid, "error:", e.stack); logging.error(req.id, "error:", e.stack);
handle_default(rid, 500, -1, userId); handle_default(-1, userId, size, def, e, callback);
} }
}; };

View File

@ -1,52 +1,26 @@
var logging = require("../logging"); var logging = require("../logging");
var helpers = require("../helpers"); var helpers = require("../helpers");
var config = require("../config");
var cache = require("../cache"); var cache = require("../cache");
var human_status = {
0: "none",
1: "cached",
2: "downloaded",
3: "checked",
"-1": "error"
};
// GET cape request // GET cape request
module.exports = function(req, res) { module.exports = function(req, callback) {
var start = new Date();
var userId = (req.url.pathname.split("/")[2] || "").split(".")[0]; var userId = (req.url.pathname.split("/")[2] || "").split(".")[0];
var etag = null; var def = req.url.query.default;
var rid = req.id; var rid = req.id;
function sendimage(rid, http_status, img_status, image) {
res.writeHead(http_status, {
"Content-Type": "image/png",
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start,
"X-Storage-Type": human_status[img_status],
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"'
});
res.end(http_status === 304 ? null : image);
}
if (!helpers.id_valid(userId)) { if (!helpers.id_valid(userId)) {
res.writeHead(422, { callback({
"Content-Type": "text/plain", status: -2,
"Response-Time": new Date() - start body: "Invalid UserID"
}); });
res.end("Invalid ID");
return; return;
} }
// strip dashes // strip dashes
userId = userId.replace(/-/g, ""); userId = userId.replace(/-/g, "");
logging.debug(rid, "userid:", userId);
try { try {
helpers.get_cape(rid, userId, function(err, status, image, hash) { helpers.get_cape(rid, userId, function(err, hash, status, image) {
logging.log(rid, "storage type:", human_status[status]);
if (err) { if (err) {
logging.error(rid, err); logging.error(rid, err);
if (err.code === "ENOENT") { if (err.code === "ENOENT") {
@ -54,38 +28,19 @@ module.exports = function(req, res) {
cache.remove_hash(rid, userId); cache.remove_hash(rid, userId);
} }
} }
etag = hash && hash.substr(0, 32) || "none"; callback({
var matches = req.headers["if-none-match"] === '"' + etag + '"'; status: status,
if (image) { body: image,
var http_status = 200; type: image ? "image/png" : undefined,
if (err) { redirect: image ? undefined : def,
http_status = 503; hash: hash,
} err: err
logging.debug(rid, "etag:", req.headers["if-none-match"]); });
logging.debug(rid, "matches:", matches);
logging.log(rid, "status:", http_status);
sendimage(rid, matches ? 304 : http_status, status, image);
} else if (matches) {
res.writeHead(304, {
"Etag": '"' + etag + '"',
"Response-Time": new Date() - start
});
res.end();
} else {
res.writeHead(404, {
"Content-Type": "text/plain",
"Etag": '"' + etag + '"',
"Response-Time": new Date() - start
});
res.end("404 not found");
}
}); });
} catch(e) { } catch(e) {
logging.error(rid, "error:" + e.stack); callback({
res.writeHead(500, { status: -1,
"Content-Type": "text/plain", err: e
"Response-Time": new Date() - start
}); });
res.end("500 server error");
} }
}; };

View File

@ -3,17 +3,16 @@ var path = require("path");
var jade = require("jade"); var jade = require("jade");
// compile jade // compile jade
var index = jade.compileFile(path.join(__dirname, "../views/index.jade")); var index = jade.compileFile(path.join(__dirname, "..", "views", "index.jade"));
module.exports = function(req, res) { module.exports = function(req, callback) {
var html = index({ var html = index({
title: "Crafatar", title: "Crafatar",
domain: "https://" + req.headers.host, domain: "https://" + req.headers.host,
config: config config: config
}); });
res.writeHead(200, { callback({
"Content-Length": Buffer.byteLength(html, "UTF-8"), body: html,
"Content-Type": "text/html; charset=utf-8" type: "text/html; charset=utf-8"
}); });
res.end(html);
}; };

View File

@ -1,115 +1,80 @@
var logging = require("../logging"); var logging = require("../logging");
var helpers = require("../helpers"); var helpers = require("../helpers");
var renders = require("../renders");
var config = require("../config"); var config = require("../config");
var cache = require("../cache"); var cache = require("../cache");
var skins = require("../skins"); var skins = require("../skins");
var renders = require("../renders"); var path = require("path");
var fs = require("fs"); var fs = require("fs");
var human_status = {
0: "none",
1: "cached",
2: "downloaded",
3: "checked",
"-1": "error"
};
// valid types: head, body // valid types: head, body
// helmet is query param // helmet is query param
// TODO: The Type logic should be two separate GET functions once response methods are extracted // TODO: The Type logic should be two separate GET functions once response methods are extracted
// GET render request // default alex/steve images can be rendered, but
module.exports = function(req, res) { // custom images will not be
var start = new Date(); function handle_default(rid, scale, helm, body, img_status, userId, size, def, err, callback) {
var raw_type = (req.url.path_list[2] || ""); if (def && def !== "steve" && def !== "alex") {
var rid = req.id; callback({
status: img_status,
// validate type redirect: def,
if (raw_type !== "body" && raw_type !== "head") { err: err
res.writeHead(422, { });
"Content-Type": "text/plain", } else {
"Response-Time": new Date() - start def = def || skins.default_skin(userId);
fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function (fs_err, buf) {
// we render the default skins, but not custom images
renders.draw_model(rid, buf, scale, helm, body, function(render_err, def_img) {
callback({
status: img_status,
body: def_img,
type: "image/png",
hash: def,
err: render_err || fs_err || err
});
});
}); });
res.end("Invalid Render Type");
return;
} }
}
// GET render request
module.exports = function(req, callback) {
var raw_type = (req.url.path_list[1] || "");
var rid = req.id;
var body = raw_type === "body"; var body = raw_type === "body";
var userId = (req.url.path_list[3] || "").split(".")[0]; var userId = (req.url.path_list[2] || "").split(".")[0];
var def = req.url.query.default; var def = req.url.query.default;
var scale = parseInt(req.url.query.scale) || config.default_scale; var scale = parseInt(req.url.query.scale) || config.default_scale;
var helm = req.url.query.hasOwnProperty("helm"); var helm = req.url.query.hasOwnProperty("helm");
var etag = null;
function sendimage(rid, http_status, img_status, image) { // validate type
logging.log(rid, "status:", http_status); if (raw_type !== "body" && raw_type !== "head") {
res.writeHead(http_status, { callback({
"Content-Type": "image/png", status: -2,
"Cache-Control": "max-age=" + config.browser_cache_time + ", public", body: "Invalid Render Type"
"Response-Time": new Date() - start,
"X-Storage-Type": human_status[img_status],
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"'
}); });
res.end(http_status === 304 ? null : image); return;
}
// default alex/steve images can be rendered, but
// custom images will not be
function handle_default(rid, http_status, img_status, userId) {
if (def && def !== "steve" && def !== "alex") {
logging.log(rid, "status: 301");
res.writeHead(301, {
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start,
"X-Storage-Type": human_status[img_status],
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Location": def
});
res.end();
} else {
def = def || skins.default_skin(userId);
fs.readFile("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);
}
// we render the default skins, but not custom images
renders.draw_model(rid, buf, scale, helm, body, function(render_err, def_img) {
if (render_err) {
logging.error(rid, "error while rendering default image:", render_err);
}
sendimage(rid, http_status, img_status, def_img);
});
});
}
} }
if (scale < config.min_scale || scale > config.max_scale) { if (scale < config.min_scale || scale > config.max_scale) {
res.writeHead(422, { callback({
"Content-Type": "text/plain", status: -2,
"Response-Time": new Date() - start body: "Invalid Scale"
}); });
res.end("422 Invalid Scale");
return; return;
} else if (!helpers.id_valid(userId)) { } else if (!helpers.id_valid(userId)) {
res.writeHead(422, { callback({
"Content-Type": "text/plain", status: -2,
"Response-Time": new Date() - start body: "Invalid UserID"
}); });
res.end("422 Invalid ID");
return; return;
} }
// strip dashes // strip dashes
userId = userId.replace(/-/g, ""); userId = userId.replace(/-/g, "");
logging.debug(rid, "userId:", userId);
try { try {
helpers.get_render(rid, userId, scale, helm, body, function(err, status, hash, image) { helpers.get_render(rid, userId, scale, helm, body, function(err, status, hash, image) {
logging.log(rid, "storage type:", human_status[status]);
if (err) { if (err) {
logging.error(rid, err); logging.error(rid, err);
if (err.code === "ENOENT") { if (err.code === "ENOENT") {
@ -117,23 +82,21 @@ module.exports = function(req, res) {
cache.remove_hash(rid, userId); cache.remove_hash(rid, userId);
} }
} }
etag = hash && hash.substr(0, 32) || "none";
var matches = req.headers["if-none-match"] === '"' + etag + '"';
if (image) { if (image) {
var http_status = 200; callback({
if (err) { status: status,
http_status = 503; body: image,
} type: "image/png",
logging.debug(rid, "etag:", req.headers["if-none-match"]); hash: hash,
logging.debug(rid, "matches:", matches); err: err
sendimage(rid, matches ? 304 : http_status, status, image); });
} else { } else {
logging.log(rid, "image not found, using default."); logging.log(rid, "image not found, using default.");
handle_default(rid, matches ? 304 : 200, status, userId); handle_default(rid, scale, helm, body, status, userId, scale, def, err, callback);
} }
}); });
} catch(e) { } catch(e) {
logging.error(rid, "error:", e.stack); logging.error(rid, "error:", e.stack);
handle_default(rid, 500, -1, userId); handle_default(rid, scale, helm, body, -1, userId, scale, def, e, callback);
} }
}; };

View File

@ -1,90 +1,79 @@
var logging = require("../logging"); var logging = require("../logging");
var helpers = require("../helpers"); var helpers = require("../helpers");
var config = require("../config");
var skins = require("../skins"); var skins = require("../skins");
var path = require("path"); var path = require("path");
var lwip = require("lwip"); var lwip = require("lwip");
function handle_default(img_status, userId, def, err, callback) {
if (def && def !== "steve" && def !== "alex") {
callback({
status: img_status,
redirect: def,
err: err
});
} else {
def = def || skins.default_skin(userId);
lwip.open(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function(lwip_err, image) {
if (image) {
image.toBuffer("png", function(buf_err, buffer) {
callback({
status: img_status,
body: buffer,
type: "image/png",
hash: def,
err: buf_err || lwip_err || err
});
});
} else {
callback({
status: -1,
err: lwip_err || err
});
}
});
}
}
// GET skin request // GET skin request
module.exports = function(req, res) { module.exports = function(req, callback) {
var start = new Date(); var userId = (req.url.path_list[1] || "").split(".")[0];
var userId = (req.url.path_list[2] || "").split(".")[0];
var def = req.url.query.default; var def = req.url.query.default;
var etag = null;
var rid = req.id; var rid = req.id;
function sendimage(rid, http_status, image) {
logging.log(rid, "status:", http_status);
res.writeHead(http_status, {
"Content-Type": "image/png",
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start,
"X-Storage-Type": "downloaded",
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"'
});
res.end(http_status === 304 ? null : image);
}
function handle_default(rid, http_status, userId) {
if (def && def !== "steve" && def !== "alex") {
logging.log(rid, "status: 301");
res.writeHead(301, {
"Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start,
"X-Storage-Type": "downloaded",
"X-Request-ID": rid,
"Access-Control-Allow-Origin": "*",
"Location": def
});
res.end();
} else {
def = def || skins.default_skin(userId);
lwip.open(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function(err, image) {
// FIXME: err is not handled
image.toBuffer("png", function(buf_err, buffer) {
// FIXME: buf_err is not handled
sendimage(rid, http_status, buffer);
});
});
}
}
if (!helpers.id_valid(userId)) { if (!helpers.id_valid(userId)) {
res.writeHead(422, { callback({
"Content-Type": "text/plain", status: -2,
"Response-Time": new Date() - start body: "Invalid UserID"
}); });
res.end("Invalid ID");
return; return;
} }
// strip dashes // strip dashes
userId = userId.replace(/-/g, ""); userId = userId.replace(/-/g, "");
logging.debug(rid, "userid:", userId);
try { try {
helpers.get_skin(rid, userId, function(err, hash, image) { helpers.get_skin(rid, userId, function(err, hash, status, image) {
if (err) { if (err) {
logging.error(rid, err); logging.error(req.id, err);
} if (err.code === "ENOENT") {
etag = hash && hash.substr(0, 32) || "none"; // no such file
var matches = req.headers["if-none-match"] === '"' + etag + '"'; cache.remove_hash(req.id, userId);
if (image) {
var http_status = 200;
if (err) {
http_status = 503;
} }
logging.debug(rid, "etag:", req.headers["if-none-match"]); }
logging.debug(rid, "matches:", matches); if (image) {
sendimage(rid, matches ? 304 : http_status, image); callback({
status: status,
body: image,
type: "image/png",
hash: hash,
err: err
});
} else { } else {
handle_default(rid, 200, userId); handle_default(2, userId, def, err, callback);
} }
}); });
} catch(e) { } catch(e) {
logging.error(rid, "error:", e.stack); logging.error(rid, "error:", e.stack);
handle_default(rid, 500, userId); handle_default(-1, userId, def, e, callback);
} }
}; };

View File

@ -1,6 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
var logging = require("./logging"); var logging = require("./logging");
var querystring = require("querystring"); var querystring = require("querystring");
var response = require("./response");
var config = require("./config"); var config = require("./config");
var http = require("http"); var http = require("http");
var mime = require("mime"); var mime = require("mime");
@ -17,63 +18,89 @@ var routes = {
capes: require("./routes/capes") capes: require("./routes/capes")
}; };
function asset_request(req, res) { // serves assets from lib/public
function asset_request(req, callback) {
var filename = path.join(__dirname, "public", req.url.path_list.join("/")); var filename = path.join(__dirname, "public", req.url.path_list.join("/"));
fs.exists(filename, function(exists) { fs.exists(filename, function(exists) {
if (exists) { if (exists) {
res.writeHead(200, { "Content-type": mime.lookup(filename) }); fs.readFile(filename, function(err, data) {
fs.createReadStream(filename).pipe(res); callback({
} else { body: data,
res.writeHead(404, { type: mime.lookup(filename),
"Content-type": "text/plain" err: err
});
}); });
res.end("Not Found"); } else {
callback({});
} }
}); });
} }
function requestHandler(req, res) { // generates a 12 character random string
var request = req; function request_id() {
request.url = url.parse(req.url, true); return Math.random().toString(36).substring(2, 14);
request.url.query = request.url.query || {}; }
// remove trailing and double slashes + other junk // splits a URL path into an Array
var path_list = request.url.pathname.split("/"); // the path is resolved and decoded
for (var i = 0; i < path_list.length; i++) { function path_list(pathname) {
// remove double and trailing slashes
pathname = pathname.replace(/\/\/+/g, "/").replace(/(.)\/$/, "$1");
var list = pathname.split("/");
list.shift();
for (var i = 0; i < list.length; i++) {
// URL decode // 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 function requestHandler(req, res) {
request.id = Math.random().toString(36).substring(2, 14); req.url = url.parse(req.url, true);
req.url.query = req.url.query || {};
req.url.path_list = path_list(req.url.pathname);
var local_path = request.url.path_list[1]; req.id = request_id();
logging.log(request.id, request.method, request.url.href); req.start = Date.now();
if (request.method === "GET" || request.method === "HEAD") {
var local_path = req.url.path_list[0];
logging.log(req.id, req.method, req.url.href);
if (req.method === "GET" || req.method === "HEAD") {
try { try {
switch (local_path) { switch (local_path) {
case "": case "":
routes.index(request, res); routes.index(req, function(result) {
response(req, res, result);
});
break; break;
case "avatars": case "avatars":
routes.avatars(request, res); routes.avatars(req, function(result) {
response(req, res, result);
});
break; break;
case "skins": case "skins":
routes.skins(request, res); routes.skins(req, function(result) {
response(req, res, result);
});
break; break;
case "renders": case "renders":
routes.renders(request, res); routes.renders(req, function(result) {
response(req, res, result);
});
break; break;
case "capes": case "capes":
routes.capes(request, res); routes.capes(req, function(result) {
response(req, res, result);
});
break; break;
default: default:
asset_request(request, res); asset_request(req, function(result) {
response(req, res, result);
});
} }
} catch(e) { } catch(e) {
var error = JSON.stringify(req.headers) + "\n" + e.stack; var error = JSON.stringify(req.headers) + "\n" + e.stack;
logging.error(request.id + "Error:", error); logging.error(req.id + "Error:", error);
res.writeHead(500, { res.writeHead(500, {
"Content-Type": "text/plain" "Content-Type": "text/plain"
}); });

View File

@ -279,16 +279,20 @@ block content
a(id="meta-x-storage-type" class="anchor") a(id="meta-x-storage-type" class="anchor")
a(href="#meta-x-storage-type") a(href="#meta-x-storage-type")
h4 X-Storage-Type h4 X-Storage-Type
p Details about how the requested image was stored on the server
ul ul
li <b>none</b>: No external requests. Cached: User has no skin. li <b>none</b>: No external requests. Cached: User has no skin.
li <b>cached</b>: No external requests. Skin cached and stored locally. li <b>cached</b>: No external requests. Skin cached and stored locally.
li li
| <b>checked</b>: 1 external request. Skin cached, checked for updates, no skin downloaded.<br> | <b>checked</b>: 1 external request. Skin cached, checked for updates, no skin downloaded.<br>
| This happens either when the user removed their skin or when it didn't change. | 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>downloaded</b>: 2 external requests. First request or skin changed, skin downloaded.
li 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. | If possible, a cached 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 section
a(id="meta-x-request-id" class="anchor") a(id="meta-x-request-id" class="anchor")
a(href="#meta-x-request-id") a(href="#meta-x-request-id")
@ -340,52 +344,54 @@ block content
// preload hover images // preload hover images
img.preload(src="/avatars/jeb_", alt="preloaded image") img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64", alt="preloaded image")
img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6", alt="preloaded image") img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/069a79f444e94726a5befca90e38aaf5?size=64", alt="preloaded image")
img.preload(src="/avatars/0?default=alex", alt="preloaded image") img.preload(src="/avatars/0?default=alex", alt="preloaded image")
img.preload(src="/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png", alt="preloaded image") img.preload(src="/avatars/0?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png", alt="preloaded image")
img.preload(src="/skins/0?default=alex", alt="preloaded image")
img.preload(src="/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8", alt="preloaded image")
img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64", alt="preloaded image")
img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64", alt="preloaded image")
img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64", alt="preloaded image") img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64", alt="preloaded image")
img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64", alt="preloaded image")
img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64", alt="preloaded image")
img.preload(src="/avatars/069a79f444e94726a5befca90e38aaf5?size=64", alt="preloaded image")
img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64", alt="preloaded image")
img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64", alt="preloaded image")
img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64", alt="preloaded image")
img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64", alt="preloaded image")
img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64", alt="preloaded image")
img.preload(src="/avatars/jeb_?size=128", alt="preloaded image")
img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64", alt="preloaded image")
img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64", alt="preloaded image")
img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64", alt="preloaded image")
img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64", alt="preloaded image")
img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64", alt="preloaded image")
img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64", alt="preloaded image")
img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64", alt="preloaded image")
img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64", alt="preloaded image")
img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64", alt="preloaded image")
img.preload(src="/avatars/jeb_?helm", alt="preloaded image")
img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm", alt="preloaded image") img.preload(src="/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64&helm", alt="preloaded image") img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64", alt="preloaded image")
img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&helm", alt="preloaded image")
img.preload(src="/renders/body/jeb_?helm&scale=4", alt="preloaded image")
img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&helm", alt="preloaded image") img.preload(src="/avatars/1c1bd09a6a0f4928a7914102a35d2670?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64&helm", alt="preloaded image") img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64", alt="preloaded image")
img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&helm", alt="preloaded image") img.preload(src="/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&helm", alt="preloaded image") img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64", alt="preloaded image")
img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&helm", alt="preloaded image") img.preload(src="/avatars/4566e69fc90748ee8d71d7ba5aa00d20?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image") img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64", alt="preloaded image")
img.preload(src="/avatars/61699b2ed3274a019f1e0ea8c3f06bc6?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64", alt="preloaded image")
img.preload(src="/avatars/696a82ce41f44b51aa31b8709b8686f0?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64", alt="preloaded image")
img.preload(src="/avatars/7125ba8b1c864508b92bb5c042ccfe2b?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64", alt="preloaded image")
img.preload(src="/avatars/7d043c7389524696bfba571c05b6aec0?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6", alt="preloaded image")
img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64", alt="preloaded image")
img.preload(src="/avatars/853c80ef3c3749fdaa49938b674adae6?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64", alt="preloaded image")
img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm", alt="preloaded image") img.preload(src="/avatars/9769ecf6331448f3ace67ae06cec64a3?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64", alt="preloaded image")
img.preload(src="/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64", alt="preloaded image")
img.preload(src="/avatars/af74a02d19cb445bb07f6866a861f783?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64", alt="preloaded image")
img.preload(src="/avatars/b05881186e75410db2db4d3066b223f7?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64", alt="preloaded image")
img.preload(src="/avatars/b9583ca43e64488a9c8c4ab27e482255?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64", alt="preloaded image")
img.preload(src="/avatars/c9b54008fd8047428b238787b5f2401c?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64", alt="preloaded image")
img.preload(src="/avatars/d8f9a4340f2d415f9acfcd70341c75ec?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64", alt="preloaded image")
img.preload(src="/avatars/e6b5c088068044df9e1b9bf11792291b?size=64&helm", alt="preloaded image")
img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64", alt="preloaded image")
img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image") img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image")
img.preload(src="/skins/jeb_", alt="preloaded image") img.preload(src="/avatars/jeb_", alt="preloaded image")
img.preload(src="/avatars/jeb_?helm", alt="preloaded image")
img.preload(src="/avatars/jeb_?size=128", alt="preloaded image")
img.preload(src="/capes/Dinnerbone", alt="preloaded image")
img.preload(src="/capes/md_5", alt="preloaded image")
img.preload(src="/renders/body/jeb_?helm&scale=4", alt="preloaded image")
img.preload(src="/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=8", alt="preloaded image")
img.preload(src="/skins/0?default=alex", alt="preloaded image")
img.preload(src="/skins/jeb_", alt="preloaded image")

View File

@ -27,17 +27,18 @@
"test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" "test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
}, },
"engines": { "engines": {
"iojs": "1.6.x" "iojs": "2.0.x"
}, },
"dependencies": { "dependencies": {
"canvas": "crafatar/node-canvas", "canvas": "crafatar/node-canvas",
"forever": "0.14.1", "forever": "0.14.1",
"jade": "~1.9.1", "jade": "~1.9.1",
"lwip": "0.0.6", "lwip": "crafatar/lwip",
"mime": "1.3.4", "mime": "1.3.4",
"node-df": "0.1.1", "node-df": "0.1.1",
"redis": "0.12.1", "redis": "0.12.1",
"request": "^2.51.0" "request": "^2.51.0",
"crc": "3.2.1"
}, },
"devDependencies": { "devDependencies": {
"coveralls": "^2.11.2", "coveralls": "^2.11.2",

View File

@ -1,21 +1,22 @@
var assert = require("assert");
var fs = require("fs");
var networking = require("../lib/networking"); var networking = require("../lib/networking");
var helpers = require("../lib/helpers"); var helpers = require("../lib/helpers");
var logging = require("../lib/logging"); var logging = require("../lib/logging");
var config = require("../lib/config");
var skins = require("../lib/skins");
var cache = require("../lib/cache");
var server = require("../lib/server");
var cleaner = require("../lib/cleaner"); var cleaner = require("../lib/cleaner");
var request = require("request"); var request = require("request");
var config = require("../lib/config");
var server = require("../lib/server");
var assert = require("assert");
var skins = require("../lib/skins");
var cache = require("../lib/cache");
var crc = require("crc").crc32;
var fs = require("fs");
// we don't want tests to fail because of slow internet // we don't want tests to fail because of slow internet
config.http_timeout *= 3; config.http_timeout *= 3;
// no spam // no spam
if (process.env.VERBOSE_TEST !== "true") { if (process.env.VERBOSE_TEST !== "true") {
logging.log = function() {}; logging.log = logging.debug = logging.warn = logging.error = function() {};
} }
var uuids = fs.readFileSync("test/uuids.txt").toString().split(/\r?\n/); var uuids = fs.readFileSync("test/uuids.txt").toString().split(/\r?\n/);
@ -140,6 +141,10 @@ describe("Crafatar", function() {
}); });
}); });
}); });
it("Username should default to Steve", function(done) {
assert.strictEqual(skins.default_skin("TestUser"), "steve");
done();
});
for (var a in alex_ids) { for (var a in alex_ids) {
var alex_id = alex_ids[a]; var alex_id = alex_ids[a];
(function(alex_id) { (function(alex_id) {
@ -211,39 +216,435 @@ describe("Crafatar", function() {
}); });
describe("Server", function() { describe("Server", function() {
// throws Exception when default headers are not in res.headers
function assert_headers(res) {
assert(res.headers["content-type"]);
assert("" + res.headers["response-time"]);
assert(res.headers["x-request-id"]);
assert.equal(res.headers["access-control-allow-origin"], "*");
assert.equal(res.headers["cache-control"], "max-age=" + config.browser_cache_time + ", public");
}
// throws Exception when +url+ is requested with +etag+
// and it does not return 304 without a body
function assert_cache(url, etag, callback) {
request.get(url, {
headers: {
"If-None-Match": etag
}
}, function(error, res, body) {
assert.ifError(error);
assert.ifError(body);
assert.equal(res.statusCode, 304);
assert(res.headers["etag"]);
assert_headers(res);
callback();
});
}
before(function(done) { before(function(done) {
server.boot(function() { server.boot(function() {
done(); done();
}); });
}); });
// Test the home page it("should return 405 Method Not Allowed for POST", function(done) {
it("should return a 200 (home page)", function(done) {
request.get("http://localhost:3000", function(error, res, body) {
assert.equal(200, res.statusCode);
done();
});
});
it("should return a 200 (asset request)", function(done) {
request.get("http://localhost:3000/stylesheets/style.css", function(error, res, body) {
assert.equal(200, res.statusCode);
done();
});
});
// invalid method, we only allow GET and HEAD requests
it("should return a 405 (invalid method)", function(done) {
request.post("http://localhost:3000", function(error, res, body) { request.post("http://localhost:3000", function(error, res, body) {
assert.equal(405, res.statusCode); assert.ifError(error);
assert.strictEqual(res.statusCode, 405);
done(); done();
}); });
}); });
it("should return correct HTTP response for home page", function(done) {
var url = "http://localhost:3000";
request.get(url, function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 200);
assert_headers(res);
assert(res.headers["etag"]);
assert.strictEqual(res.headers["content-type"], "text/html; charset=utf-8");
assert.strictEqual(res.headers["etag"], "\"" + crc(body) + "\"");
assert(body);
assert_cache(url, res.headers["etag"], function() {
done();
});
});
});
it("should return correct HTTP response for assets", function(done) {
var url = "http://localhost:3000/stylesheets/style.css";
request.get(url, function(error, res, body) {
assert.ifError(error);
assert.strictEqual(res.statusCode, 200);
assert_headers(res);
assert(res.headers["etag"]);
assert.strictEqual(res.headers["content-type"], "text/css");
assert.strictEqual(res.headers["etag"], "\"" + crc(body) + "\"");
assert(body);
assert_cache(url, res.headers["etag"], function() {
done();
});
});
});
var server_tests = {
"avatar with existing username": {
url: "http://localhost:3000/avatars/jeb_?size=16",
etag: '"a846b82963"',
crc32: 1623808067
},
"avatar with not existing username": {
url: "http://localhost:3000/avatars/0?size=16",
etag: '"steve"',
crc32: 2416827277
},
"avatar with not existing username defaulting to alex": {
url: "http://localhost:3000/avatars/0?size=16&default=alex",
etag: '"alex"',
crc32: 862751081
},
"avatar with not existing username defaulting to url": {
url: "http://localhost:3000/avatars/0?size=16&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"helm avatar with existing username": {
url: "http://localhost:3000/avatars/jeb_?size=16&helm",
etag: '"a846b82963"',
crc32: 646871998
},
"helm avatar with not existing username": {
url: "http://localhost:3000/avatars/0?size=16&helm",
etag: '"steve"',
crc32: 2416827277
},
"helm avatar with not existing username defaulting to alex": {
url: "http://localhost:3000/avatars/0?size=16&helm&default=alex",
etag: '"alex"',
crc32: 862751081
},
"helm avatar with not existing username defaulting to url": {
url: "http://localhost:3000/avatars/0?size=16&helm&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"avatar with existing uuid": {
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
etag: '"a846b82963"',
crc32: 1623808067
},
"avatar with not existing uuid": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16",
etag: '"steve"',
crc32: 2416827277
},
"avatar with not existing uuid defaulting to alex": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=alex",
etag: '"alex"',
crc32: 862751081
},
"avatar with not existing uuid defaulting to url": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"helm avatar with existing uuid": {
url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&helm",
etag: '"a846b82963"',
crc32: 646871998
},
"helm avatar with not existing uuid": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm",
etag: '"steve"',
crc32: 2416827277
},
"helm avatar with not existing uuid defaulting to alex": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=alex",
etag: '"alex"',
crc32: 862751081
},
"helm avatar with not existing uuid defaulting to url": {
url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&helm&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"cape with existing username": {
url: "http://localhost:3000/capes/jeb_",
etag: '"3f688e0e69"',
crc32: 989800403
},
"cape with not existing username": {
url: "http://localhost:3000/capes/0",
crc32: 0
},
"cape with not existing username defaulting to url": {
url: "http://localhost:3000/capes/0?default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"cape with existing uuid": {
url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6",
etag: '"3f688e0e69"',
crc32: 989800403
},
"cape with not existing uuid": {
url: "http://localhost:3000/capes/00000000000000000000000000000000",
crc32: 0
},
"cape with not existing uuid defaulting to url": {
url: "http://localhost:3000/capes/00000000000000000000000000000000?default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"skin with existing username": {
url: "http://localhost:3000/skins/jeb_",
etag: '"a846b82963"',
crc32: 110922424
},
"skin with not existing username": {
url: "http://localhost:3000/skins/0",
etag: '"steve"',
crc32: 981937087
},
"skin with not existing username defaulting to alex": {
url: "http://localhost:3000/skins/0?default=alex",
etag: '"alex"',
crc32: 2298915739
},
"skin with not existing username defaulting to url": {
url: "http://localhost:3000/skins/0?default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"skin with existing uuid": {
url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6",
etag: '"a846b82963"',
crc32: 110922424
},
"skin with not existing uuid": {
url: "http://localhost:3000/skins/00000000000000000000000000000000",
etag: '"steve"',
crc32: 981937087
},
"skin with not existing uuid defaulting to alex": {
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=alex",
etag: '"alex"',
crc32: 2298915739
},
"skin with not existing uuid defaulting to url": {
url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"head render with existing username": {
url: "http://localhost:3000/renders/head/jeb_?scale=2",
etag: '"a846b82963"',
crc32: [353633671, 370672768]
},
"head render with not existing username": {
url: "http://localhost:3000/renders/head/0?scale=2",
etag: '"steve"',
crc32: [883439147, 433083528]
},
"head render with not existing username defaulting to alex": {
url: "http://localhost:3000/renders/head/0?scale=2&default=alex",
etag: '"alex"',
crc32: [1240086237, 1108800327]
},
"head render with not existing username defaulting to url": {
url: "http://localhost:3000/renders/head/0?scale=2&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"helm head render with existing username": {
url: "http://localhost:3000/renders/head/jeb_?scale=2&helm",
etag: '"a846b82963"',
crc32: [3456497067, 3490318764]
},
"helm head render with not existing username": {
url: "http://localhost:3000/renders/head/0?scale=2&helm",
etag: '"steve"',
crc32: [1858563554, 2647471936]
},
"helm head render with not existing username defaulting to alex": {
url: "http://localhost:3000/renders/head/0?scale=2&helm&default=alex",
etag: '"alex"',
crc32: [2963161105, 1769904825]
},
"helm head render with not existing username defaulting to url": {
url: "http://localhost:3000/renders/head/0?scale=2&helm&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"head render with existing uuid": {
url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2",
etag: '"a846b82963"',
crc32: [353633671, 370672768]
},
"head render with not existing uuid": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2",
etag: '"steve"',
crc32: [883439147, 433083528]
},
"head render with not existing uuid defaulting to alex": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=alex",
etag: '"alex"',
crc32: [1240086237, 1108800327]
},
"head render with not existing uuid defaulting to url": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"helm head render with existing uuid": {
url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm",
etag: '"a846b82963"',
crc32: [3456497067, 3490318764]
},
"helm head render with not existing uuid": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm",
etag: '"steve"',
crc32: [1858563554, 2647471936]
},
"helm head render with not existing uuid defaulting to alex": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=alex",
etag: '"alex"',
crc32: [2963161105, 1769904825]
},
"helm head render with not existing uuid defaulting to url": {
url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"body render with existing username": {
url: "http://localhost:3000/renders/body/jeb_?scale=2",
etag: '"a846b82963"',
crc32: [1291941229, 2628108474]
},
"body render with not existing username": {
url: "http://localhost:3000/renders/body/0?scale=2",
etag: '"steve"',
crc32: [2652947188, 2115706574]
},
"body render with not existing username defaulting to alex": {
url: "http://localhost:3000/renders/body/0?scale=2&default=alex",
etag: '"alex"',
crc32: [407932087, 2516216042]
},
"body render with not existing username defaulting to url": {
url: "http://localhost:3000/renders/body/0?scale=2&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"helm body render with existing username": {
url: "http://localhost:3000/renders/body/jeb_?scale=2&helm",
etag: '"a846b82963"',
crc32: [3556188297, 4269754007]
},
"helm body render with not existing username": {
url: "http://localhost:3000/renders/body/0?scale=2&helm",
etag: '"steve"',
crc32: [272191039, 542896675]
},
"helm body render with not existing username defaulting to alex": {
url: "http://localhost:3000/renders/body/0?scale=2&helm&default=alex",
etag: '"alex"',
crc32: [737759773, 66512449]
},
"helm body render with not existing username defaulting to url": {
url: "http://localhost:3000/renders/body/0?scale=2&helm&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"body render with existing uuid": {
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
etag: '"a846b82963"',
crc32: [1291941229, 2628108474]
},
"body render with not existing uuid": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2",
etag: '"steve"',
crc32: [2652947188, 2115706574]
},
"body render with not existing uuid defaulting to alex": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=alex",
etag: '"alex"',
crc32: [407932087, 2516216042]
},
"body render with not existing uuid defaulting to url": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
"helm body render with existing uuid": {
url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm",
etag: '"a846b82963"',
crc32: [3556188297, 4269754007]
},
"helm body render with not existing uuid": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm",
etag: '"steve"',
crc32: [272191039, 542896675]
},
"helm body render with not existing uuid defaulting to alex": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=alex",
etag: '"alex"',
crc32: [737759773, 66512449]
},
"helm body render with not existing uuid defaulting to url": {
url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http://example.com",
crc32: 0,
redirect: "http://example.com"
},
};
for (var description in server_tests) {
var location = server_tests[description];
(function(location) {
it("should return correct HTTP response for " + description, function(done) {
request.get(location.url, {followRedirect: false, encoding: null}, function(error, res, body) {
assert.ifError(error);
assert_headers(res);
assert(res.headers["x-storage-type"]);
assert.strictEqual(res.headers["etag"], location.etag);
var matches = false;
if (location.crc32 instanceof Array) {
for (var i = 0; i < location.crc32.length; i++) {
if (location.crc32[i] === crc(body)) {
matches = true;
break;
}
}
} else {
matches = (location.crc32 === crc(body));
}
assert.ok(matches);
assert.strictEqual(res.headers["location"], location.redirect);
if (location.etag === undefined) {
assert.strictEqual(res.statusCode, location.redirect ? 307 : 404);
assert.strictEqual(res.headers["content-type"], "text/plain");
done();
} else {
assert(res.headers["etag"]);
assert.strictEqual(res.headers["content-type"], "image/png");
assert.strictEqual(res.statusCode, 200);
assert_cache(location.url, res.headers["etag"], function() {
done();
});
}
});
});
}(location));
}
it("should return a 422 (invalid size)", function(done) { it("should return a 422 (invalid size)", function(done) {
var size = config.max_size + 1; var size = config.max_size + 1;
request.get("http://localhost:3000/avatars/Jake_0?size=" + size, function(error, res, body) { request.get("http://localhost:3000/avatars/Jake_0?size=" + size, function(error, res, body) {
assert.equal(422, res.statusCode); assert.strictEqual(res.statusCode, 422);
done(); done();
}); });
}); });
@ -251,74 +652,26 @@ describe("Crafatar", function() {
it("should return a 422 (invalid scale)", function(done) { it("should return a 422 (invalid scale)", function(done) {
var scale = config.max_scale + 1; var scale = config.max_scale + 1;
request.get("http://localhost:3000/renders/head/Jake_0?scale=" + scale, function(error, res, body) { request.get("http://localhost:3000/renders/head/Jake_0?scale=" + scale, function(error, res, body) {
assert.equal(422, res.statusCode); assert.strictEqual(res.statusCode, 422);
done();
});
});
// no default images for capes, should 404
it("should return a 404 (no cape)", function(done) {
request.get("http://localhost:3000/capes/Jake_0", function(error, res, body) {
assert.equal(404, res.statusCode);
done(); done();
}); });
}); });
it("should return a 422 (invalid render type)", function(done) { it("should return a 422 (invalid render type)", function(done) {
request.get("http://localhost:3000/renders/side/Jake_0", function(error, res, body) { request.get("http://localhost:3000/renders/invalid/Jake_0", function(error, res, body) {
assert.equal(422, res.statusCode); assert.strictEqual(res.statusCode, 422);
done(); done();
}); });
}); });
// testing all paths for valid inputs // testing all paths for Invalid UserID
var locations = ["avatars", "skins", "renders/head"]; var locations = ["avatars", "skins", "capes", "renders/body", "renders/head"];
for (var l in locations) { for (var l in locations) {
var location = locations[l]; var location = locations[l];
(function(location) { (function(location) {
it("should return a 200 (valid input " + location + ")", function(done) {
request.get("http://localhost:3000/" + location + "/Jake_0", function(error, res, body) {
assert.equal(200, res.statusCode);
done();
});
});
it("should return a 422 (invalid id " + location + ")", function(done) { it("should return a 422 (invalid id " + location + ")", function(done) {
request.get("http://localhost:3000/" + location + "/thisisaninvaliduuid", function(error, res, body) { request.get("http://localhost:3000/" + location + "/thisisaninvaliduuid", function(error, res, body) {
assert.equal(422, res.statusCode); assert.strictEqual(res.statusCode, 422);
done();
});
});
})(location);
}
// testing all paths for invalid id formats
locations = ["avatars", "capes", "skins", "renders/head"];
for (l in locations) {
var location = locations[l];
(function(location) {
it("should return a 422 (invalid id " + location + ")", function(done) {
request.get("http://localhost:3000/" + location + "/thisisaninvaliduuid", function(error, res, body) {
assert.equal(422, res.statusCode);
done();
});
});
})(location);
}
//testing all paths for default images
locations = ["avatars", "skins", "renders/head"];
for (l in locations) {
var location = locations[l];
(function(location) {
it("should return a 200 (default steve image " + location + ")", function(done) {
request.get("http://localhost:3000/" + location + "/invalidjsvns?default=steve", function(error, res, body) {
assert.equal(200, res.statusCode);
done();
});
});
it("should return a 200 (default external image " + location + ")", function(done) {
request.get("http://localhost:3000/" + location + "/invalidjsvns?default=https%3A%2F%2Fi.imgur.com%2FocJVWAc.png", function(error, res, body) {
assert.equal(200, res.statusCode);
done(); done();
}); });
}); });
@ -350,7 +703,7 @@ describe("Crafatar", function() {
describe("Networking: Cape", function() { describe("Networking: Cape", function() {
it("should not fail (guaranteed cape)", function(done) { it("should not fail (guaranteed cape)", function(done) {
helpers.get_cape(rid, "Dinnerbone", function(err, hash, img) { helpers.get_cape(rid, "Dinnerbone", function(err, hash, status, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
@ -359,13 +712,13 @@ describe("Crafatar", function() {
before(function() { before(function() {
cache.get_redis().flushall(); cache.get_redis().flushall();
}); });
helpers.get_cape(rid, "Dinnerbone", function(err, hash, img) { helpers.get_cape(rid, "Dinnerbone", function(err, hash, status, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
}); });
it("should not be found", function(done) { it("should not be found", function(done) {
helpers.get_cape(rid, "Jake_0", function(err, hash, img) { helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) {
assert.strictEqual(img, null); assert.strictEqual(img, null);
done(); done();
}); });
@ -374,7 +727,7 @@ describe("Crafatar", function() {
describe("Networking: Skin", function() { describe("Networking: Skin", function() {
it("should not fail", function(done) { it("should not fail", function(done) {
helpers.get_cape(rid, "Jake_0", function(err, hash, img) { helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
@ -383,7 +736,7 @@ describe("Crafatar", function() {
before(function() { before(function() {
cache.get_redis().flushall(); cache.get_redis().flushall();
}); });
helpers.get_cape(rid, "Jake_0", function(err, hash, img) { helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
@ -432,7 +785,7 @@ describe("Crafatar", function() {
describe("Networking: Skin", function() { describe("Networking: Skin", function() {
it("should not fail (uuid)", function(done) { it("should not fail (uuid)", function(done) {
helpers.get_skin(rid, id, function(err, hash, img) { helpers.get_skin(rid, id, function(err, hash, status, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
@ -456,7 +809,7 @@ describe("Crafatar", function() {
describe("Networking: Cape", function() { describe("Networking: Cape", function() {
it("should not fail (possible cape)", function(done) { it("should not fail (possible cape)", function(done) {
helpers.get_cape(rid, id, function(err, hash, img) { helpers.get_cape(rid, id, function(err, hash, status, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });