Network rewrite/major cleanup, major caching changes, etc

Work on implementing capes
Update to no-express
Add more render tests
Add capes express route
Add documentation
Add undefined images to .gitignore
Fix no-express server again
Cleanup config a bit
General cleanup
Add CodeClimate badge
Test on Heroku
Speed optimizations
Fix tests
Code cleanup
This commit is contained in:
Jake 2015-01-02 01:21:15 -06:00
parent dd7e46f377
commit 3def0910bc
23 changed files with 566 additions and 478 deletions

1
.gitignore vendored
View File

@ -5,3 +5,4 @@ node_modules/
*.rdb *.rdb
coverage/ coverage/
modules/config.js modules/config.js
undefined*.png

View File

@ -1,4 +1,4 @@
# Crafatar [![travis](https://img.shields.io/travis/Jake0oo0/crafatar.svg?style=flat)](https://travis-ci.org/Jake0oo0/crafatar/) [![Coverage Status](https://img.shields.io/coveralls/Jake0oo0/crafatar.svg?style=flat)](https://coveralls.io/r/Jake0oo0/crafatar) # Crafatar [![travis](https://img.shields.io/travis/Jake0oo0/crafatar.svg?style=flat)](https://travis-ci.org/Jake0oo0/crafatar/) [![Coverage Status](https://img.shields.io/coveralls/Jake0oo0/crafatar.svg?style=flat)](https://coveralls.io/r/Jake0oo0/crafatar) [![Code Climate](https://codeclimate.com/github/Jake0oo0/crafatar/badges/gpa.svg)](https://codeclimate.com/github/Jake0oo0/crafatar)
https://crafatar.com https://crafatar.com

63
app.js
View File

@ -1,63 +0,0 @@
var express = require("express");
var path = require("path");
var logger = require("morgan");
var cookieParser = require("cookie-parser");
var bodyParser = require("body-parser");
var routes = require("./routes/index");
var avatars = require("./routes/avatars");
var skins = require("./routes/skins");
var renders = require('./routes/renders');
var capes = require("./routes/capes");
var app = express();
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use('/', routes);
app.use('/avatars', avatars);
app.use('/skins', skins);
app.use('/renders', renders);
app.use("/capes", capes);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
var err = new Error("Not Found");
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get("env") === "development") {
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render("error", {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500);
res.render("error", {
message: err.message,
error: {}
});
});
module.exports = app;

View File

@ -103,14 +103,16 @@ exp.update_timestamp = function(uuid, hash) {
}; };
// create the key +uuid+, store +hash+ and time // create the key +uuid+, store +hash+ and time
exp.save_hash = function(uuid, hash) { exp.save_hash = function(uuid, skin, cape) {
logging.log(uuid + " cache: saving hash"); logging.log(uuid + " cache: saving hash");
logging.log("skin:" + skin + " cape:" + cape);
var time = new Date().getTime(); var time = new Date().getTime();
// store shorter null byte instead of "null" // store shorter null byte instead of "null"
hash = hash || "."; skin = skin || ".";
cape = cape || ".";
// store uuid in lower case if not null // store uuid in lower case if not null
uuid = uuid && uuid.toLowerCase(); uuid = uuid && uuid.toLowerCase();
redis.hmset(uuid, "h", hash, "t", time); redis.hmset(uuid, "s", skin, "c", cape, "t", time);
}; };
exp.remove_hash = function(uuid) { exp.remove_hash = function(uuid) {
@ -119,7 +121,7 @@ exp.remove_hash = function(uuid) {
}; };
// get a details object for +uuid+ // get a details object for +uuid+
// {hash: "0123456789abcdef", time: 1414881524512} // {skin: "0123456789abcdef", cape: "gs1gds1g5d1g5ds1", time: 1414881524512}
// null when uuid unkown // null when uuid unkown
exp.get_details = function(uuid, callback) { exp.get_details = function(uuid, callback) {
// get uuid in lower case if not null // get uuid in lower case if not null
@ -128,7 +130,8 @@ exp.get_details = function(uuid, callback) {
var details = null; var details = null;
if (data) { if (data) {
details = { details = {
hash: (data.h == "." ? null : data.h), skin: (data.s === "." ? null : data.s),
cape: (data.c === "." ? null : data.c),
time: Number(data.t) time: Number(data.t)
}; };
} }

View File

@ -15,7 +15,7 @@ function should_clean_redis(callback) {
callback(err, false); callback(err, false);
} else { } else {
try { try {
logging.debug(info); //logging.debug(info.toString());
logging.debug("used mem:" + info.used_memory); logging.debug("used mem:" + info.used_memory);
var used = parseInt(info.used_memory) / 1024; var used = parseInt(info.used_memory) / 1024;
logging.log("RedisCleaner: " + used + "KB used"); logging.log("RedisCleaner: " + used + "KB used");
@ -71,7 +71,6 @@ exp.run = function() {
var helmdir = __dirname + "/../" + config.helms_dir; var helmdir = __dirname + "/../" + config.helms_dir;
var renderdir = __dirname + "/../" + config.renders_dir; var renderdir = __dirname + "/../" + config.renders_dir;
var skindir = __dirname + "/../" + config.skins_dir; var skindir = __dirname + "/../" + config.skins_dir;
fs.readdir(facesdir, function (err, files) { fs.readdir(facesdir, function (err, files) {
for (var i = 0, l = Math.min(files.length, config.cleaning_amount); i < l; i++) { for (var i = 0, l = Math.min(files.length, config.cleaning_amount); i < l; i++) {
var filename = files[i]; var filename = files[i];
@ -98,4 +97,4 @@ exp.run = function() {
function nil () {} function nil () {}
module.exports = exp; module.exports = exp;

View File

@ -2,6 +2,9 @@ var config = {
min_size: 1, // for avatars min_size: 1, // for avatars
max_size: 512, // for avatars; too big values might lead to slow response time or DoS max_size: 512, // for avatars; too big values might lead to slow response time or DoS
default_size: 160, // for avatars; size to be used when no size given default_size: 160, // for avatars; size to be used when no size given
min_scale: 1, // for renders
max_scale: 10, // for renders; too big values might lead to slow response time or DoS
default_scale: 6, // for renders; scale to be used when no scale given
local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response local_cache_time: 1200, // seconds until we will check if the image changed. should be > 60 to prevent mojang 429 response
browser_cache_time: 3600, // seconds until browser will request image again browser_cache_time: 3600, // seconds until browser will request image again
cleaning_interval: 1800, // seconds interval: deleting images if disk size at limit cleaning_interval: 1800, // seconds interval: deleting images if disk size at limit
@ -9,15 +12,12 @@ var config = {
cleaning_redis_limit: 24576, // max allowed used KB on redis to trigger redis flush cleaning_redis_limit: 24576, // max allowed used KB on redis to trigger redis flush
cleaning_amount: 50000, // amount of avatar (and their helm) files to clean cleaning_amount: 50000, // amount of avatar (and their helm) files to clean
http_timeout: 1000, // ms until connection to mojang is dropped http_timeout: 1000, // ms until connection to mojang is dropped
debug_enabled: false, // enables logging.debug
faces_dir: "images/faces/", // directory where faces are kept. should have trailing "/" faces_dir: "images/faces/", // directory where faces are kept. should have trailing "/"
helms_dir: "images/helms/", // directory where helms are kept. should have trailing "/" helms_dir: "images/helms/", // directory where helms are kept. should have trailing "/"
skins_dir: "images/skins/", // directory where skins are kept. should have trailing "/" skins_dir: "images/skins/", // directory where skins are kept. should have trailing "/"
renders_dir: "images/renders/",// Directory where rendered skins are kept. should have trailing "/" renders_dir: "images/renders/",// Directory where rendered skins are kept. should have trailing "/"
capes_dir: "images/capes/", // directory where capes are kept. should have trailing "/" capes_dir: "images/capes/", // directory where capes are kept. should have trailing "/"
debug_enabled: false, // enables logging.debug
min_scale: 1, // for renders
max_scale: 10, // for renders; too big values might lead to slow response time or DoS
default_scale: 6 // for renders; scale to be used when no scale given
}; };
module.exports = config; module.exports = config;

View File

@ -10,112 +10,145 @@ var fs = require("fs");
var valid_uuid = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username var valid_uuid = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username
var hash_pattern = /[0-9a-f]+$/; var hash_pattern = /[0-9a-f]+$/;
// gets the hash from the textures.minecraft.net +url+
function get_hash(url) { function get_hash(url) {
return hash_pattern.exec(url)[0].toLowerCase(); return hash_pattern.exec(url)[0].toLowerCase();
} }
// requests skin for +uuid+ and extracts face/helm if image hash in +details+ changed function store_skin(uuid, profile, details, callback) {
// callback contains error, image hash networking.get_skin_url(uuid, profile, function(url) {
function store_images(uuid, details, callback) { if (url) {
// get skin_url for +uuid+ var hash = get_hash(url);
networking.get_skin_url(uuid, function(err, skin_url) { if (details && details.skin === hash) {
if (err) { cache.update_timestamp(uuid, hash);
callback(err, null); callback(null, hash);
} else {
if (skin_url) {
logging.log(uuid + " " + skin_url);
// set file paths
var hash = get_hash(skin_url);
if (details && details.hash == hash) {
// hash hasn't changed
logging.log(uuid + " hash has not changed");
cache.update_timestamp(uuid, hash);
callback(null, hash);
} else {
// hash has changed
logging.log(uuid + " new hash: " + hash);
var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png";
fs.exists(facepath, function (exists) {
if (exists) {
logging.log(uuid + " Avatar already exists, not downloading");
cache.save_hash(uuid, hash);
callback(null, hash);
} else {
// download skin
networking.get_skin(skin_url, uuid, function(err, img) {
if (err || !img) {
callback(err, null);
} else {
// extract face / helm
skins.extract_face(img, facepath, function(err) {
if (err) {
callback(err);
} else {
logging.log(uuid + " face extracted");
logging.debug(uuid + " " + facepath);
skins.extract_helm(uuid, facepath, img, helmpath, function(err) {
logging.log(uuid + " helm extracted");
logging.debug(uuid + " " + helmpath);
cache.save_hash(uuid, hash);
callback(err, hash);
});
}
});
}
});
}
});
}
} else { } else {
// profile found, but has no skin logging.log(uuid + " new skin hash: " + hash);
cache.save_hash(uuid, null); var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
callback(null, null); var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png"
} fs.exists(facepath, function(exists) {
} if (exists) {
}); logging.log(uuid + " skin already exists, not downloading");
networking.get_cape_url(uuid, function(err, cape_url) {
if (err) {
callback(err, null);
} else {
if (cape_url) {
logging.log(uuid + " " + cape_url);
// set file paths
var hash = get_hash(cape_url);
if (details && details.hash == hash) {
// hash hasn't changed
logging.log(uuid + " hash has not changed");
cache.update_timestamp(uuid, hash);
callback(null, hash);
} else {
// hash has changed
logging.log(uuid + " new hash: " + hash);
var capepath = __dirname + "/../" + config.capes_dir + hash + ".png";
if (fs.existsSync(capepath)) {
logging.log(uuid + " Cape already exists, not downloading");
cache.save_hash(uuid, hash);
callback(null, hash); callback(null, hash);
} else { } else {
// download cape networking.get_from(url, function(img, response, err) {
networking.get_cape(cape_url, function(err, img) {
if (err || !img) { if (err || !img) {
callback(err, null); callback(err, null);
} else {
skins.extract_face(img, facepath, function(err) {
if (err) {
logging.error(err);
callback(err, null);
} else {
logging.log(uuid + " face extracted");
skins.extract_helm(facepath, img, helmpath, function(err) {
logging.log(uuid + " helm extracted");
logging.debug(helmpath);
callback(err, hash);
});
}
});
} }
}); });
} }
} });
} else {
// profile found, but has no cape
cache.save_hash(uuid, null);
callback(null, null);
} }
} else {
callback(null, null);
} }
}); });
} }
function store_cape(uuid, profile, details, callback) {
networking.get_cape_url(uuid, profile, function(url) {
if (url) {
var hash = get_hash(url);
if (details && details.cape === hash) {
cache.update_timestamp(uuid, hash);
callback(null, hash);
} else {
logging.log(uuid + " new cape hash: " + hash);
var capepath = __dirname + "/../" + config.capes_dir + hash + ".png";
fs.exists(capepath, function(exists) {
if (exists) {
logging.log(uuid + " cape already exists, not downloading");
callback(null, hash);
} else {
networking.get_from(url, function(img, response, err) {
if (err || !img) {
logging.error(err);
callback(err, null);
} else {
skins.save_image(img, capepath, function(err) {
logging.log(uuid + " cape saved");
callback(err, hash);
});
}
});
}
});
}
} else {
callback(null, null);
}
});
}
function remove_from_array(arr, item) {
var i;
while((i = arr.indexOf(item)) !== -1) {
arr.splice(i, 1);
}
}
// downloads the images for +uuid+ while checking the cache
// status based on +details+. +whichhash+ specifies which
// image is more important, and should be called back on
// +callback+ contains the error buffer and image hash
var currently_running = [];
function callback_for(uuid, which, err, cape_hash, skin_hash) {
for (var i = 0; i < currently_running.length; i++) {
if (currently_running[i] && currently_running[i].uuid === uuid && (currently_running[i].which === which || which === null)) {
var will_call = currently_running[i];
will_call.callback(err, will_call.which === 'skin' ? skin_hash : cape_hash);
//remove_from_array(currently_running, i);
delete(currently_running[i]);
}
}
}
function array_has_hash(arr, property, value) {
for (var i = 0; i < arr.length; i++) {
if (arr[i] && arr[i][property] === value) {
return true;
}
}
return false;
}
function store_images(uuid, details, whichhash, callback) {
var isUUID = uuid.length > 16;
var new_hash = { 'uuid': uuid, 'which': whichhash, 'callback': callback };
if (!array_has_hash(currently_running, 'uuid', uuid)) {
currently_running.push(new_hash);
networking.get_profile((isUUID ? uuid : null), function(err, profile) {
if (err || (isUUID && !profile)) {
callback_for(uuid, err, null, null);
} else {
store_skin(uuid, profile, details, function(err, skin_hash) {
cache.save_hash(uuid, skin_hash, null);
callback_for(uuid, 'skin', err, null, skin_hash);
store_cape(uuid, profile, details, function(err, cape_hash) {
cache.save_hash(uuid, skin_hash, cape_hash);
callback_for(uuid, 'cape', err, cape_hash, skin_hash);
});
});
}
});
} else {
currently_running.push(new_hash);
}
}
var exp = {}; var exp = {};
@ -125,7 +158,6 @@ exp.uuid_valid = function(uuid) {
return valid_uuid.test(uuid); return valid_uuid.test(uuid);
}; };
// decides whether to get an image from disk or to download it // decides whether to get an image from disk or to download it
// callback contains error, status, hash // callback contains error, status, hash
// the status gives information about how the image was received // the status gives information about how the image was received
@ -134,49 +166,33 @@ exp.uuid_valid = function(uuid) {
// 1: "cached" - found on disk // 1: "cached" - found on disk
// 2: "downloaded" - profile downloaded, skin downloaded from mojang servers // 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 // 3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
exp.get_image_hash = function(uuid, callback) { exp.get_image_hash = function(uuid, raw_type, callback) {
cache.get_details(uuid, function(err, details) { cache.get_details(uuid, function(err, details) {
var type = (details !== null ? (raw_type === "skin" ? details.skin : details.cape) : null);
if (err) { if (err) {
callback(err, -1, null); callback(err, -1, null);
} else { } else {
if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) { if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {logging.log(uuid + " uuid cached & recently updated");
// uuid known + recently updated callback(null, (type ? 1 : 0), type);
logging.log(uuid + " uuid cached & recently updated"); } else {
callback(null, (details.hash ? 1 : 0), details.hash); if (details) {
logging.log(uuid + " uuid cached, but too old");
} else { } else {
if (details) { logging.log(uuid + " uuid not cached");
logging.log(uuid + " uuid cached, but too old"); }
store_images(uuid, details, raw_type, function(err, hash) {
if (err) {
callback(err, -1, details && type);
} else { } else {
logging.log(uuid + " uuid not cached"); var status = details && (type === hash) ? 3 : 2;
logging.debug(uuid + " old hash: " + (details && type));
logging.log(uuid + " hash: " + hash);
callback(null, status, hash);
} }
store_images(uuid, details, function(err, hash) { });
if (err) {
callback(err, -1, details && details.hash);
} else {
// skin is only checked (3) when uuid known AND hash didn't change
// in all other cases the skin is downloaded (2)
var status = details && (details.hash == hash) ? 3 : 2;
logging.debug(uuid + " old hash: " + (details && details.hash));
logging.log(uuid + " hash: " + hash);
callback(null, status, hash);
}
});
}
} }
}); }
}; });
exp.get_cape_hash = function(uuid, callback) {
cache.get_details(uuid, function(err, details) {
if (err) {
callback(err, -1, null);
} else {
if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
logging.log(uuid + " uuid cached & recently updated");
}
}
})
}; };
@ -185,17 +201,16 @@ exp.get_cape_hash = function(uuid, callback) {
// image is the user's face+helm when helm is true, or the face otherwise // image is the user's face+helm when helm is true, or the face otherwise
// for status, see get_image_hash // for status, see get_image_hash
exp.get_avatar = function(uuid, helm, size, callback) { exp.get_avatar = function(uuid, helm, size, callback) {
exp.get_image_hash(uuid, function(err, status, hash) { logging.log("request: " + uuid);
exp.get_image_hash(uuid, "skin", function(err, status, hash) {
if (hash) { if (hash) {
var facepath = __dirname + "/../" + config.faces_dir + hash + ".png"; var facepath = __dirname + "/../" + config.faces_dir + hash + ".png";
var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png"; var helmpath = __dirname + "/../" + config.helms_dir + hash + ".png";
var filepath = facepath; var filepath = facepath;
fs.exists(helmpath, function (exists) { fs.exists(helmpath, function (exists) {
if (helm && exists) { if (helm && exists) {
filepath = helmpath; filepath = helmpath;
} }
skins.resize_img(filepath, size, function(img_err, result) { skins.resize_img(filepath, size, function(img_err, result) {
if (img_err) { if (img_err) {
callback(img_err, -1, null, hash); callback(img_err, -1, null, hash);
@ -216,7 +231,8 @@ exp.get_avatar = function(uuid, helm, size, callback) {
// handles requests for +uuid+ skins // handles requests for +uuid+ skins
// callback contains error, hash, image buffer // callback contains error, hash, image buffer
exp.get_skin = function(uuid, callback) { exp.get_skin = function(uuid, callback) {
exp.get_image_hash(uuid, function(err, status, hash) { logging.log(uuid + " skin request");
exp.get_image_hash(uuid, 'skin', function(err, status, hash) {
var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png"; var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png";
fs.exists(skinpath, function (exists) { fs.exists(skinpath, function (exists) {
if (exists) { if (exists) {
@ -235,7 +251,7 @@ exp.get_skin = function(uuid, callback) {
function get_type(helm, body) { function get_type(helm, body) {
var text = body ? "body" : "head"; var text = body ? "body" : "head";
return helm ? text+"helm" : text; return helm ? text + "helm" : text;
} }
// handles creations of skin renders // handles creations of skin renders
@ -247,51 +263,84 @@ exp.get_render = function(uuid, scale, helm, body, callback) {
return; return;
} }
var renderpath = __dirname + "/../" + config.renders_dir + hash + "-" + scale + "-" + get_type(helm, body) + ".png"; var renderpath = __dirname + "/../" + config.renders_dir + hash + "-" + scale + "-" + get_type(helm, body) + ".png";
fs.exists(renderpath, function (exists) { fs.exists(renderpath, function(exists) {
if (exists) { if (exists) {
renders.open_render(uuid, renderpath, function(err, img) { renders.open_render(uuid, renderpath, function(err, img) {
callback(err, 1, hash, img); callback(err, 1, hash, img);
}); });
return; return;
} } else {
if (!img) { if (!img) {
callback(err, 0, hash, null); callback(err, 0, hash, null);
return; return;
}
renders.draw_model(uuid, img, scale, helm, body, function(err, img) {
if (err) {
callback(err, -1, hash, null);
} else if (!img) {
callback(null, 0, hash, null);
} else {
fs.writeFile(renderpath, img, "binary", function(err){
if (err) {
logging.log(uuid + " error: " + err);
}
callback(null, 2, hash, img);
});
} }
}); renders.draw_model(uuid, img, scale, helm, body, function(err, img) {
if (err) {
callback(err, -1, hash, null);
} else if (!img) {
callback(null, 0, hash, null);
} else {
fs.writeFile(renderpath, img, "binary", function(err) {
if (err) {
logging.log(err);
}
callback(null, 2, hash, img);
});
}
});
}
}); });
}); });
}; };
// handles requests for +uuid+ skins
// callback contains error, hash, image buffer
exp.get_skin = function(uuid, callback) {
logging.log(uuid + " skin request");
exp.get_image_hash(uuid, "skin", function(err, status, hash) {
var skinpath = __dirname + "/../" + config.skins_dir + hash + ".png";
fs.exists(skinpath, function(exists) {
if (exists) {
logging.log("skin already exists, not downloading");
skins.open_skin(skinpath, function(err, img) {
callback(err, hash, img);
});
} else {
networking.save_texture(uuid, hash, skinpath, function(err, response, img) {
callback(err, hash, img);
});
}
});
});
};
// handles requests for +uuid+ capes
// callback contains error, hash, image buffer
exp.get_cape = function(uuid, callback) { exp.get_cape = function(uuid, callback) {
logging.log(uuid + " cape request"); logging.log(uuid + " cape request");
exp.get_image_hash(uuid, function(err, status, hash) { exp.get_image_hash(uuid, "cape", function(err, status, hash) {
if (hash) { if (!hash) {
var capeurl = "http://textures.minecraft.net/texture/" + hash;
networking.get_cape(capeurl, function(err, img) {
if (err) {
logging.error("error while downloading cape");
callback(err, hash, null);
} else {
callback(null, hash, img);
}
});
} else {
callback(err, null, null); callback(err, null, null);
return;
} }
var capepath = __dirname + "/../" + config.capes_path + hash + ".png";
fs.exists(capepath, function(exists) {
if (exists) {
logging.log("cape already exists, not downloading");
skins.open_skin(capepath, function(err, img) {
callback(err, hash, img);
});
} else {
networking.save_texture(uuid, hash, capepath, function(err, response, img) {
if (response && response.statusCode === 404) {
callback(err, hash, null);
} else {
callback(err, hash, img);
}
});
}
});
}); });
}; };

View File

@ -1,224 +1,195 @@
var logging = require("./logging"); var logging = require("./logging");
var request = require("request"); var request = require("requestretry");
var config = require("./config"); var config = require("./config");
var skins = require("./skins");
var fs = require("fs"); var fs = require("fs");
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/"; var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
var skins_url = "https://skins.minecraft.net/MinecraftSkins/"; var skins_url = "https://skins.minecraft.net/MinecraftSkins/";
var capes_url = "https://skins.minecraft.net/MinecraftCloaks/"; var capes_url = "https://skins.minecraft.net/MinecraftCloaks/";
// exracts the skin url of a +profile+ object
// returns null when no url found (user has no skin)
function extract_skin_url(profile) {
var url = null;
if (profile && profile.properties) {
profile.properties.forEach(function(prop) {
if (prop.name == "textures") {
var json = Buffer(prop.value, "base64").toString();
var props = JSON.parse(json);
url = props && props.textures && props.textures.SKIN && props.textures.SKIN.url || null;
}
});
}
return url;
}
function extract_cape_url(profile) {
var url = null;
if (profile && profile.properties) {
profile.properties.forEach(function(prop) {
if (prop.name == "textures") {
var json = Buffer(prop.value, "base64").toString();
var props = JSON.parse(json);
url = props && props.textures && props.textures.CAPE && props.textures.CAPE.url || null;
}
});
}
return url;
}
// make a request to skins.miencraft.net
// the skin url is taken from the HTTP redirect
var get_username_url = function(name, callback) {
request.get({
url: skins_url + name + ".png",
headers: {
"User-Agent": "https://crafatar.com"
},
timeout: config.http_timeout,
followRedirect: false
}, function(error, response, body) {
if (!error && response.statusCode == 301) {
// skin_url received successfully
logging.log(name + " skin url received");
callback(null, response.headers.location);
} else if (error) {
callback(error, null);
} else if (response.statusCode == 404) {
// skin (or user) doesn't exist
logging.log(name + " has no skin");
callback(null, null);
} else if (response.statusCode == 429) {
// Too Many Requests
// Never got this, seems like skins aren't limited
logging.warn(name + body || "Too many requests");
callback(null, null);
} else {
logging.error(name + " Unknown error:");
logging.error(name + " " + response);
callback(body || "Unknown error", null);
}
});
};
// make a request to sessionserver
// the skin_url is taken from the profile
var get_uuid_url = function(uuid, callback) {
request.get({
url: session_url + uuid,
headers: {
"User-Agent": "https://crafatar.com"
},
timeout: config.http_timeout // ms
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
// profile downloaded successfully
logging.log(uuid + " profile downloaded");
callback(null, extract_skin_url(JSON.parse(body)));
} else if (error) {
callback(error, null);
} else if (response.statusCode == 204 || response.statusCode == 404) {
// we get 204 No Content when UUID doesn't exist (including 404 in case they change that)
logging.log(uuid + " uuid does not exist");
callback(null, null);
} else if (response.statusCode == 429) {
// Too Many Requests
callback(body || "Too many requests", null);
} else {
logging.error(uuid + " Unknown error:");
logging.error(uuid + " " + response);
callback(body || "Unknown error", null);
}
});
};
var exp = {}; var exp = {};
// download skin_url for +uuid+ (name or uuid) function extract_url(profile, property) {
// callback contains error, skin_url var url = null;
exp.get_skin_url = function(uuid, callback) { if (profile && profile.properties) {
if (uuid.length <= 16) { profile.properties.forEach(function(prop) {
get_username_url(uuid, function(err, url) { if (prop.name === "textures") {
callback(err, url); var json = new Buffer(prop.value, "base64").toString();
}); var props = JSON.parse(json);
} else { url = props && props.textures && props.textures[property] && props.textures[property].url || null;
get_uuid_url(uuid, function(err, url) { }
callback(err, url);
}); });
} }
return url;
}; };
exp.get_cape_url = function(uuid, callback) { // exracts the skin url of a +profile+ object
if (uuid.length <= 16) { // returns null when no url found (user has no skin)
get_username_url(uuid, function(err, url) { exp.extract_skin_url = function(profile) {
callback(err, url); return extract_url(profile, 'SKIN');
});
} else {
get_uuid_url(uuid, function(err, url) {
callback(err, url);
});
}
}; };
// downloads skin file from +url+ // exracts the cape url of a +profile+ object
// callback contains error, image // returns null when no url found (user has no cape)
exp.get_skin = function(url, uuid, callback) { exp.extract_cape_url = function(profile) {
return extract_url(profile, 'CAPE');
};
// makes a GET request to the +url+
// +options+ hash includes various options for
// encoding and timeouts, defaults are already
// specified. +callback+ contains the body, response,
// and error buffer. get_from helper method is available
exp.get_from_options = function(url, options, callback) {
request.get({ request.get({
url: url, url: url,
headers: { headers: {
"User-Agent": "https://crafatar.com" "User-Agent": "https://crafatar.com"
}, },
encoding: null, // encoding must be null so we get a buffer timeout: (options.timeout || config.http_timeout),
timeout: config.http_timeout // ms encoding: (options.encoding || null),
}, function (error, response, body) { followRedirect: (options.folow_redirect || false),
if (!error && response.statusCode == 200) { maxAttempts: 2,
// skin downloaded successfully retryDelay: 2000,
logging.log(uuid + " downloaded skin"); retryStrategy: request.RetryStrategies.NetworkError
logging.debug(uuid + " " + url); }, function(error, response, body) {
callback(null, body); if (!error && (response.statusCode === 200 || response.statusCode === 301)) {
// skin_url received successfully
logging.log(url + " url received");
callback(body, response, null);
} else if (error) {
callback(body || null, response, error);
} else if (response.statusCode === 404) {
// page doesn't exist
logging.log(url + " url does not exist");
callback(null, response, null);
} else if (response.statusCode === 429) {
// Too Many Requests exception - code 429
logging.warn(body || "Too many requests");
callback(body || null, response, error);
} else { } else {
if (error) { logging.error(url + " Unknown error:");
logging.error(uuid + " error downloading '" + url + "': " + error); //logging.error(response);
} else if (response.statusCode == 404) { callback(body || null, response, error);
logging.warn(uuid + " texture not found (404): " + url);
} else if (response.statusCode == 429) {
// Too Many Requests
// Never got this, seems like textures aren't limited
logging.warn(uuid + " too many requests for " + url);
logging.warn(uuid + " " + body);
} else {
logging.error(uuid + " unknown error for " + url);
logging.error(uuid + " " + response);
logging.error(uuid + " " + body);
error = "unknown error"; // Error needs to be set, otherwise null in callback
}
callback(error, null);
} }
}); });
}; };
exp.save_skin = function(uuid, hash, outpath, callback) { // helper method for get_from_options, no options required
exp.get_from = function(url, callback) {
exp.get_from_options(url, {}, function(body, response, err) {
callback(body, response, err);
});
};
// specifies which numbers identify what url
var mojang_url_types = {
1: skins_url,
2: capes_url
};
// make a request to skins.miencraft.net
// the skin url is taken from the HTTP redirect
// type reference is above
exp.get_username_url = function(name, type, callback) {
exp.get_from(mojang_url_types[type] + name + ".png", function(body, response, err) {
if (!err) {
callback(err, response ? (response.statusCode === 404 ? null : response.headers.location) : null);
} else {
callback(err, null);
}
});
};
// gets the URL for a skin/cape from the profile
// +type+ specifies which to retrieve
exp.get_uuid_url = function(profile, type, callback) {
var url = null;
if (type === 1) {
url = exp.extract_skin_url(profile);
} else if (type === 2) {
url = exp.extract_cape_url(profile);
}
callback(url || null);
};
// make a request to sessionserver
// profile is returned as json
exp.get_profile = function(uuid, callback) {
if (!uuid) {
callback(null, null);
} else {
exp.get_from_options(session_url + uuid, {encoding: "utf8"} ,function(body, response, err) {
callback(err !== null ? err : null, (body !== null ? JSON.parse(body) : null));
});
}
};
// todo remove middleman
// +uuid+ is likely a username and if so
// +uuid+ is used to get the url, otherwise
// +profile+ will be used to get the url
exp.get_skin_url = function(uuid, profile, callback) {
getUrl(uuid, profile, 1, function(url) {
callback(url);
});
};
// +uuid+ is likely a username and if so
// +uuid+ is used to get the url, otherwise
// +profile+ will be used to get the url
exp.get_cape_url = function(uuid, profile, callback) {
getUrl(uuid, profile, 2, function(url) {
callback(url);
});
};
function getUrl(uuid, profile, type, callback) {
if (uuid.length <= 16) {
//username
exp.get_username_url(uuid, type, function(err, url) {
callback(url || null);
});
} else {
exp.get_uuid_url(profile, type, function(url) {
callback(url || null);
});
}
}
// downloads skin file from +url+
// callback contains error, image
exp.get_skin = function(url, callback) {
exp.get_from(url, function(body, response, err) {
callback(body, err);
});
};
exp.save_texture = function(uuid, hash, outpath, callback) {
if (hash) { if (hash) {
var skinurl = "http://textures.minecraft.net/texture/" + hash; var textureurl = "http://textures.minecraft.net/texture/" + hash;
exp.get_skin(skinurl, uuid, function(err, img) { exp.get_from(textureurl, function(img, response, err) {
if (err) { if (err) {
logging.error(uuid + " error while downloading skin"); logging.error(uuid + "error while downloading texture");
callback(err, null); callback(err, response, null);
} else { } else {
fs.writeFile(outpath, img, "binary", function(err){ fs.writeFile(outpath, img, "binary", function(err) {
if (err) { if (err) {
logging.log(uuid + " error: " + err); logging.log(uuid + " error: " + err);
} }
callback(null, img); callback(err, response, img);
}); });
} }
}); });
} else { } else {
callback(null, null); callback(null, null, null);
} }
}; };
exp.get_cape = function(url, callback) { exp.get_cape = function(url, callback) {
request.get({ exp.get_from(url, function(body, response, err) {
url: url, callback(err, body);
headers: {
"User-Agent": "https://crafatar.com"
},
encoding: null, // encoding must be null so we get a buffer
timeout: config.http_timeout // ms
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
// cape downloaded successfully
logging.log("downloaded cape");
logging.debug(url);
callback(null, body);
} else {
if (error) {
logging.error("Error downloading '" + url + "': " + error);
} else if (response.statusCode == 404) {
logging.warn("texture not found (404): " + url);
} else if (response.statusCode == 429) {
logging.warn("too many requests for " + url);
logging.warn(body);
} else {
logging.error("unknown error for " + url);
logging.error(response);
logging.error(body);
error = "unknown error"; // Error needs to be set, otherwise null in callback
}
callback(error, null);
}
}); });
}; };

View File

@ -140,7 +140,7 @@ exp.draw_model = function(uuid, img, scale, helm, body, callback) {
image.onload = function() { image.onload = function() {
var width = 64 * scale; var width = 64 * scale;
var original_height = (image.height == 32 ? 32 : 64); var original_height = (image.height === 32 ? 32 : 64);
var height = original_height * scale; var height = original_height * scale;
var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale); var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale);
var skin_canvas = new Canvas(width, height); var skin_canvas = new Canvas(width, height);
@ -199,4 +199,4 @@ function scale_image(imageData, context, d_x, d_y, scale) {
} }
} }
module.exports = exp; module.exports = exp;

View File

@ -104,4 +104,21 @@ exp.open_skin = function(uuid, skinpath, callback) {
}); });
}; };
exp.save_image = function(buffer, outpath, callback) {
lwip.open(buffer, "png", function(err, image) {
if (err) {
callback(err);
} else {
image.batch()
.writeFile(outpath, function(err) {
if (err) {
callback(err);
} else {
callback(null);
}
});
}
});
};
module.exports = exp; module.exports = exp;

View File

@ -30,7 +30,7 @@
"jade": "~1.8.2", "jade": "~1.8.2",
"lwip": "0.0.6", "lwip": "0.0.6",
"redis": "0.12.1", "redis": "0.12.1",
"request": "2.51.0", "requestretry": "1.2.2",
"node-df": "0.1.1", "node-df": "0.1.1",
"mime": "1.2.11" "mime": "1.2.11"
}, },

View File

@ -195,6 +195,13 @@ h4 {
background-image: url("/skins/0?default=alex"); background-image: url("/skins/0?default=alex");
} }
#cape-example-1:hover .preview {
background-image: url("/capes/Dinnerbone");
}
#cape-example-2:hover .preview {
background-image: url("/capes/md_5");
}
img.preload { img.preload {
/* /*
preload hover images preload hover images
@ -284,4 +291,4 @@ img.preload {
.avatar.flipped { .avatar.flipped {
-webkit-transform: rotate(180deg); -webkit-transform: rotate(180deg);
transform: rotate(180deg); transform: rotate(180deg);
} }

View File

@ -54,7 +54,7 @@ module.exports = function(req, res) {
} }
} }
etag = image && hash && hash.substr(0, 32) || "none"; etag = image && hash && hash.substr(0, 32) || "none";
var matches = req.headers["if-none-match"] == '"' + etag + '"'; var matches = req.headers["if-none-match"] === '"' + etag + '"';
if (image) { if (image) {
var http_status = 200; var http_status = 200;
if (matches) { if (matches) {
@ -103,6 +103,6 @@ module.exports = function(req, res) {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"' "Etag": '"' + etag + '"'
}); });
res.end(http_status == 304 ? null : image); res.end(http_status === 304 ? null : image);
} }
}; };

View File

@ -1,18 +1,27 @@
var logging = require("../modules/logging"); var logging = require("../modules/logging");
var helpers = require("../modules/helpers"); var helpers = require("../modules/helpers");
var config = require("../modules/config"); var config = require("../modules/config");
var router = require("express").Router();
var lwip = require("lwip");
/* GET skin request. */ var human_status = {
router.get("/:uuid.:ext?", function (req, res) { 0: "none",
var uuid = (req.params.uuid || ""); 1: "cached",
var def = req.query.default; 2: "downloaded",
3: "checked",
"-1": "error"
};
// GET cape request
module.exports = function(req, res) {
var start = new Date(); var start = new Date();
var uuid = (req.url.pathname.split("/")[2] || "").split(".")[0];
var etag = null; var etag = null;
if (!helpers.uuid_valid(uuid)) { if (!helpers.uuid_valid(uuid)) {
res.status(422).send("422 Invalid UUID"); res.writeHead(422, {
"Content-Type": "text/plain",
"Response-Time": new Date() - start
});
res.end("Invalid ID");
return; return;
} }
@ -20,13 +29,13 @@ router.get("/:uuid.:ext?", function (req, res) {
uuid = uuid.replace(/-/g, ""); uuid = uuid.replace(/-/g, "");
try { try {
helpers.get_cape(uuid, function (err, hash, image) { helpers.get_cape(uuid, function(err, status, image, hash) {
logging.log(uuid); logging.log(uuid + " - " + human_status[status]);
if (err) { if (err) {
logging.error(err); logging.error(uuid + " " + err);
} }
etag = hash && hash.substr(0, 32) || "none"; etag = hash && hash.substr(0, 32) || "none";
var matches = req.get("If-None-Match") == "\"" + etag + "\""; var matches = req.headers["if-none-match"] === '"' + etag + '"';
if (image) { if (image) {
var http_status = 200; var http_status = 200;
if (matches) { if (matches) {
@ -34,33 +43,37 @@ router.get("/:uuid.:ext?", function (req, res) {
} else if (err) { } else if (err) {
http_status = 503; http_status = 503;
} }
logging.debug("Etag: " + req.get("If-None-Match")); logging.debug("Etag: " + req.headers["if-none-match"]);
logging.debug("matches: " + matches); logging.debug("matches: " + matches);
logging.log("status: " + http_status); logging.log("status: " + http_status);
sendimage(http_status, image); sendimage(http_status, status, image);
} else { } else {
res.status(404).send("404 not found"); res.writeHead(404, {
"Content-Type": "text/plain",
"Response-Time": new Date() - start
});
res.end("404 not found");
} }
}); });
} catch (e) { } catch(e) {
logging.error("Error!"); logging.error(uuid + " error:");
logging.error(e); logging.error(e);
res.status(500).send("500 error while retrieving cape"); res.writeHead(500, {
"Content-Type": "text/plain",
"Response-Time": new Date() - start
});
res.end("500 server error");
} }
function sendimage(http_status, img_status, image) {
function sendimage(http_status, image) {
res.writeHead(http_status, { res.writeHead(http_status, {
"Content-Type": "image/png", "Content-Type": "image/png",
"Cache-Control": "max-age=" + config.browser_cache_time + ", public", "Cache-Control": "max-age=" + config.browser_cache_time + ", public",
"Response-Time": new Date() - start, "Response-Time": new Date() - start,
"X-Storage-Type": "downloaded", "X-Storage-Type": human_status[img_status],
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Etag": "\"" + etag + "\"" "Etag": '"' + etag + '"'
}); });
res.end(http_status == 304 ? null : image); res.end(http_status === 304 ? null : image);
} }
}); };
module.exports = router;

View File

@ -23,7 +23,7 @@ module.exports = function(req, res) {
var raw_type = (req.url.path_list[2] || ""); var raw_type = (req.url.path_list[2] || "");
// validate type // validate type
if (raw_type != "body" && raw_type != "head") { if (raw_type !== "body" && raw_type !== "head") {
res.writeHead(422, { res.writeHead(422, {
"Content-Type": "text/plain", "Content-Type": "text/plain",
"Response-Time": new Date() - start "Response-Time": new Date() - start
@ -32,7 +32,7 @@ module.exports = function(req, res) {
return; return;
} }
var body = raw_type == "body"; var body = raw_type === "body";
var uuid = (req.url.path_list[3] || "").split(".")[0]; var uuid = (req.url.path_list[3] || "").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;
@ -65,7 +65,7 @@ module.exports = function(req, res) {
logging.error(uuid + " " + err); logging.error(uuid + " " + err);
} }
etag = hash && hash.substr(0, 32) || "none"; etag = hash && hash.substr(0, 32) || "none";
var matches = req.headers["if-none-match"] == '"' + etag + '"'; var matches = req.headers["if-none-match"] === '"' + etag + '"';
if (image) { if (image) {
var http_status = 200; var http_status = 200;
if (matches) { if (matches) {
@ -128,6 +128,6 @@ module.exports = function(req, res) {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"' "Etag": '"' + etag + '"'
}); });
res.end(http_status == 304 ? null : image); res.end(http_status === 304 ? null : image);
} }
}; };

View File

@ -31,7 +31,7 @@ module.exports = function(req, res) {
logging.error(uuid + " " + err); logging.error(uuid + " " + err);
} }
etag = hash && hash.substr(0, 32) || "none"; etag = hash && hash.substr(0, 32) || "none";
var matches = req.headers["if-none-match"] == '"' + etag + '"'; var matches = req.headers["if-none-match"] === '"' + etag + '"';
if (image) { if (image) {
var http_status = 200; var http_status = 200;
if (matches) { if (matches) {
@ -82,6 +82,6 @@ module.exports = function(req, res) {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
"Etag": '"' + etag + '"' "Etag": '"' + etag + '"'
}); });
res.end(http_status == 304 ? null : image); res.end(http_status === 304 ? null : image);
} }
}; };

View File

@ -5,7 +5,6 @@ var config = require("./modules/config");
var clean = require("./modules/cleaner"); var clean = require("./modules/cleaner");
var http = require("http"); var http = require("http");
var mime = require("mime"); var mime = require("mime");
var path = require("path");
var url = require("url"); var url = require("url");
var fs = require("fs"); var fs = require("fs");
@ -13,7 +12,8 @@ var routes = {
index: require("./routes/index"), index: require("./routes/index"),
avatars: require("./routes/avatars"), avatars: require("./routes/avatars"),
skins: require("./routes/skins"), skins: require("./routes/skins"),
renders: require("./routes/renders") renders: require("./routes/renders"),
capes: require("./routes/capes")
}; };
function asset_request(req, res) { function asset_request(req, res) {
@ -37,7 +37,7 @@ function requestHandler(req, res) {
request.url.query = request.url.query || {}; request.url.query = request.url.query || {};
// remove trailing and double slashes + other junk // remove trailing and double slashes + other junk
var path_list = path.resolve(request.url.pathname).split("/"); var path_list = request.url.pathname.split("/");
for (var i = 0; i < path_list.length; i++) { for (var i = 0; i < path_list.length; i++) {
// URL decode // URL decode
path_list[i] = querystring.unescape(path_list[i]); path_list[i] = querystring.unescape(path_list[i]);
@ -61,6 +61,9 @@ function requestHandler(req, res) {
case "renders": case "renders":
routes.renders(request, res); routes.renders(request, res);
break; break;
case "capes":
routes.capes(request, res);
break;
default: default:
asset_request(request, res); asset_request(request, res);
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

View File

@ -14,4 +14,8 @@ for uuid in `cat "$dir/uuids.txt"`; do
helm="&helm" helm="&helm"
fi fi
curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://$host/avatars/$uuid?size=$size$helm" curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://$host/avatars/$uuid?size=$size$helm"
done <<<<<<< HEAD
done
=======
done
>>>>>>> Network rewrite/major cleanup, major caching changes, etc

View File

@ -8,6 +8,7 @@ var config = require("../modules/config");
var skins = require("../modules/skins"); var skins = require("../modules/skins");
var cache = require("../modules/cache"); var cache = require("../modules/cache");
var renders = require("../modules/renders"); var renders = require("../modules/renders");
var cleaner = require("../modules/cleaner")
// 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;
@ -22,6 +23,10 @@ var names = fs.readFileSync("test/usernames.txt").toString().split(/\r?\n/);
var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))]; var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))];
var name = names[Math.round(Math.random() * (names.length - 1))]; var name = names[Math.round(Math.random() * (names.length - 1))];
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var ids = [ var ids = [
uuid.toLowerCase(), uuid.toLowerCase(),
uuid.toUpperCase(), uuid.toUpperCase(),
@ -35,6 +40,7 @@ describe("Crafatar", function() {
before(function() { before(function() {
cache.get_redis().flushall(); cache.get_redis().flushall();
cleaner.run();
}); });
describe("UUID/username", function() { describe("UUID/username", function() {
@ -79,13 +85,14 @@ describe("Crafatar", function() {
done(); done();
}); });
it("should not exist (uuid)", function(done) { it("should not exist (uuid)", function(done) {
networking.get_skin_url("00000000000000000000000000000000", function(err, profile) { var number = getRandomInt(0, 9).toString();
assert.strictEqual(err, null); networking.get_profile(Array(33).join(number), function(err, profile) {
assert.strictEqual(profile, null);
done(); done();
}); });
}); });
it("should not exist (username)", function(done) { it("should not exist (username)", function(done) {
networking.get_skin_url("Steve", function(err, profile) { networking.get_username_url("Steve", 1, function(err, profile) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
@ -99,10 +106,13 @@ describe("Crafatar", function() {
var steven_uuid = "b8ffc3d37dbf48278f69475f6690aabd"; var steven_uuid = "b8ffc3d37dbf48278f69475f6690aabd";
it("uuid's account should exist, but skin should not", function(done) { it("uuid's account should exist, but skin should not", function(done) {
helpers.get_avatar(alex_uuid, false, 160, function(err, status, image) { networking.get_profile(alex_uuid, function(err, profile) {
assert.strictEqual(status, 2); assert.notStrictEqual(profile, null);
done(); networking.get_uuid_url(profile, 1, function(url) {
}); assert.strictEqual(url, null);
done();
});
})
}); });
it("odd UUID should default to Alex", function(done) { it("odd UUID should default to Alex", function(done) {
assert.strictEqual(skins.default_skin(alex_uuid), "alex"); assert.strictEqual(skins.default_skin(alex_uuid), "alex");
@ -117,7 +127,7 @@ describe("Crafatar", function() {
it("should time out on uuid info download", function(done) { it("should time out on uuid info download", function(done) {
var original_timeout = config.http_timeout; var original_timeout = config.http_timeout;
config.http_timeout = 1; config.http_timeout = 1;
networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, skin_url) { networking.get_profile("069a79f444e94726a5befca90e38aaf5", function(err, profile) {
assert.strictEqual(err.code, "ETIMEDOUT"); assert.strictEqual(err.code, "ETIMEDOUT");
config.http_timeout = original_timeout; config.http_timeout = original_timeout;
done(); done();
@ -126,7 +136,7 @@ describe("Crafatar", function() {
it("should time out on username info download", function(done) { it("should time out on username info download", function(done) {
var original_timeout = config.http_timeout; var original_timeout = config.http_timeout;
config.http_timeout = 1; config.http_timeout = 1;
networking.get_skin_url("redstone_sheep", function(err, skin_url) { networking.get_username_url("redstone_sheep", 1, function(err, url) {
assert.strictEqual(err.code, "ETIMEDOUT"); assert.strictEqual(err.code, "ETIMEDOUT");
config.http_timeout = original_timeout; config.http_timeout = original_timeout;
done(); done();
@ -135,7 +145,11 @@ describe("Crafatar", function() {
it("should time out on skin download", function(done) { it("should time out on skin download", function(done) {
var original_timeout = config.http_timeout; var original_timeout = config.http_timeout;
config.http_timeout = 1; config.http_timeout = 1;
<<<<<<< HEAD
networking.get_skin("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", uuid, function(err, img) { networking.get_skin("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", uuid, function(err, img) {
=======
networking.get_from("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(img, response, err) {
>>>>>>> Network rewrite/major cleanup, major caching changes, etc
assert.strictEqual(err.code, "ETIMEDOUT"); assert.strictEqual(err.code, "ETIMEDOUT");
config.http_timeout = original_timeout; config.http_timeout = original_timeout;
done(); done();
@ -143,7 +157,11 @@ describe("Crafatar", function() {
}); });
it("should not find the skin", function(done) { it("should not find the skin", function(done) {
assert.doesNotThrow(function() { assert.doesNotThrow(function() {
<<<<<<< HEAD
networking.get_skin("http://textures.minecraft.net/texture/this-does-not-exist", uuid, function(err, img) { networking.get_skin("http://textures.minecraft.net/texture/this-does-not-exist", uuid, function(err, img) {
=======
networking.get_from("http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) {
>>>>>>> Network rewrite/major cleanup, major caching changes, etc
assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions
done(); done();
}); });
@ -157,6 +175,32 @@ describe("Crafatar", function() {
}); });
}); });
// we have to make sure that we test both a 32x64 and 64x64 skin
describe("Networking: Render", function() {
it("should not fail (username, 32x64 skin)", function(done) {
helpers.get_render("md_5", 6, true, true, function(err, hash, img) {
assert.strictEqual(err, null);
done();
});
});
it("should not fail (username, 64x64 skin)", function(done) {
helpers.get_render("Jake0oo0", 6, true, true, function(err, hash, img) {
assert.strictEqual(err, null);
done();
});
});
});
describe("Networking: Cape", function() {
it("should not fail (guaranteed cape)", function(done) {
helpers.get_cape("Dinnerbone", function(err, hash, img) {
assert.strictEqual(err, null);
done();
});
});
});
// DRY with uuid and username tests // DRY with uuid and username tests
for (var i in ids) { for (var i in ids) {
var id = ids[i]; var id = ids[i];
@ -177,6 +221,7 @@ describe("Crafatar", function() {
}); });
it("should be cached", function(done) { it("should be cached", function(done) {
helpers.get_avatar(id, false, 160, function(err, status, image) { helpers.get_avatar(id, false, 160, function(err, status, image) {
console.log("STATUS: " + status)
assert.strictEqual(status === 0 || status === 1, true); assert.strictEqual(status === 0 || status === 1, true);
done(); done();
}); });
@ -206,17 +251,23 @@ describe("Crafatar", function() {
}); });
describe("Networking: Render", function() { describe("Networking: Render", function() {
it("should not fail (username, 64x64 skin)", function(done) { it("should not fail (full body)", function(done) {
helpers.get_render("Jake0oo0", 6, true, true, function(err, hash, img) { helpers.get_render(id, 6, true, true, function(err, hash, img) {
assert.strictEqual(err, null);
done();
});
});
it("should not fail (only head)", function(done) {
helpers.get_render(id, 6, true, false, function(err, hash, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
}); });
}); });
describe("Networking: Render", function() { describe("Networking: Cape", function() {
it("should not fail (username, 32x64 skin)", function(done) { it("should not fail (possible cape)", function(done) {
helpers.get_render("md_5", 6, true, true, function(err, hash, img) { helpers.get_cape(id, function(err, hash, img) {
assert.strictEqual(err, null); assert.strictEqual(err, null);
done(); done();
}); });
@ -231,8 +282,9 @@ describe("Crafatar", function() {
if (id_type == "uuid") { if (id_type == "uuid") {
it("uuid should be rate limited", function(done) { it("uuid should be rate limited", function(done) {
helpers.get_avatar(id, false, 160, function(err, status, image) { networking.get_profile(id, function(err, profile) {
assert.strictEqual(JSON.parse(err).error, "TooManyRequestsException"); console.log("THIS THING:: " + err)
assert.strictEqual(profile.error, "TooManyRequestsException");
done(); done();
}); });
}); });

View File

@ -220,6 +220,34 @@ block content
| Hover over the examples for a preview! | Hover over the examples for a preview!
.preview-background .preview-background
section
a(id="capes" class="anchor")
a(href="#capes")
h3 Capes
p
| A cape endpoint is also available to get the active cape of a user.<br>
| Replace
mark.green id
| with a Mojang <b>UUID</b> or <b>username</b> to get the related cape.<br>
| The user's cape is returned, otherwise a 404 is thrown.<br>
.code
| #{domain}/skins/
mark.green id
section
a(id="cape-examples", class="anchor")
a(href="#cape-examples")
h4 Cape Examples
.code
#cape-example-1.example-wrapper
.example #{domain}/capes/Dinnerbone
p.preview Dinnerbone's Cape <i>Mojang capes are not transparent...</i>
#cape-example-2.example-wrapper
.example #{domain}/capes/md_5
p.preview md_5's Cape
p.preview-placeholder
| Hover over the examples for a preview!
.preview-background
section section
a(id="meta" class="anchor") a(id="meta" class="anchor")
@ -343,4 +371,8 @@ block content
img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", alt="preloaded image") img.preload(src="/avatars/020242a17b9441799eff511eea1221da?size=64&helm", 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/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image") img.preload(src="/avatars/f8cdb6839e9043eea81939f85d9c5d69?size=64&helm", alt="preloaded image")
<<<<<<< HEAD
img.preload(src="/skins/jeb_", alt="preloaded image") img.preload(src="/skins/jeb_", alt="preloaded image")
=======
img.preload(src="/skins/jeb_", alt="preloaded image")
>>>>>>> Network rewrite/major cleanup, major caching changes, etc