From e6481e3c73af3640b97f66204cafe59c0ebd3d16 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 23 Nov 2014 21:53:42 +0100 Subject: [PATCH] bug fixes, improvements, fix #13, fix #14 --- modules/config.js | 4 +-- modules/helpers.js | 62 +++++++++++----------------------- modules/networking.js | 78 ++++++++++++++++++++++++++++++++++++++----- routes/avatars.js | 4 +-- test/test.js | 34 ++++++++++++------- 5 files changed, 114 insertions(+), 68 deletions(-) diff --git a/modules/config.js b/modules/config.js index 9b15725..e5501f7 100644 --- a/modules/config.js +++ b/modules/config.js @@ -4,9 +4,9 @@ var config = { default_size: 160, // size to be used when no size given local_cache_time: 3600, // 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 - http_timeout: 1000, // ms until connection to mojang is dropped + http_timeout: 3000, // ms until connection to mojang is dropped faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' helms_dir: 'skins/helms/' // directory where helms are kept. should have trailing '/' }; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/modules/helpers.js b/modules/helpers.js index 2a2183a..7d2d70e 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -5,7 +5,7 @@ var skins = require('./skins'); // 0098cb60-fa8e-427c-b299-793cbd302c9a var valid_uuid = /^([0-9a-f-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username -var hash_pattern = /([^\/]+)(?=\.\w{0,16}$)|((?:[a-z][a-z]*[0-9]+[a-z0-9]*))/; +var hash_pattern = /[0-9a-f]+$/; function get_hash(url) { return hash_pattern.exec(url)[0].toLowerCase(); @@ -14,28 +14,15 @@ function get_hash(url) { // requests skin for +uuid+ and extracts face/helm if image hash in +details+ changed // callback contains error, image hash function store_images(uuid, details, callback) { - // get profile for +uuid+ - networking.get_profile(uuid, function(err, profile) { - if (err === 0) { - // uuid does not exist - cache.save_hash(uuid, null); - callback(null, null); - } else if (err) { + // get skin_url for +uuid+ + networking.get_skin_url(uuid, function(err, skin_url) { + if (err) { callback(err, null); } else { - var skinurl = null; - - // Username handling - if (uuid.length <= 16) { - skinurl = "https://skins.minecraft.net/MinecraftSkins/" + uuid + ".png"; - console.log(uuid + " is a username"); - } else { - skinurl = skin_url(profile); - } - if (skinurl) { - console.log(uuid + " " + skinurl); + if (skin_url) { + console.log(uuid + " " + skin_url); // set file paths - var hash = get_hash(skinurl); + var hash = get_hash(skin_url); if (details && details.hash == hash) { // hash hasn't changed console.log(uuid + " hash has not changed"); @@ -47,7 +34,7 @@ function store_images(uuid, details, callback) { var facepath = __dirname + '/../' + config.faces_dir + hash + ".png"; var helmpath = __dirname + '/../' + config.helms_dir + hash + ".png"; // download skin, extract face/helm - networking.skin_file(skinurl, facepath, helmpath, function(err) { + networking.skin_file(skin_url, facepath, helmpath, function(err) { if (err) { callback(err, null); } else { @@ -65,30 +52,14 @@ function store_images(uuid, details, callback) { }); } -// exracts the skin url of a +profile+ object -// returns null when no url found (user has no skin) -function 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; -} - // decides whether to get an image from disk or to download it // callback contains error, status, hash // the status gives information about how the image was received -// -1: error -// 0: cached as null -// 1: found on disk -// 2: profile requested/found, skin downloaded from mojang servers -// 3: profile requested/found, but it has not changed or no skin +// -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 function get_image_hash(uuid, callback) { cache.get_details(uuid, function(err, details) { if (err) { @@ -100,12 +71,17 @@ function get_image_hash(uuid, callback) { callback(null, (details.hash ? 1 : 0), details.hash); } else { console.log(uuid + " uuid not known or too old"); + console.log("details:"); + console.log(details); + console.log("/details"); store_images(uuid, details, function(err, hash) { if (err) { callback(err, -1, details && details.hash); } else { console.log(uuid + " hash: " + hash); - callback(null, (hash != (details && details.hash) ? 2 : 3), hash); + var oldhash = details && details.hash; + var status = hash !== oldhash ? 2 : 3; + callback(null, status, hash); } }); } diff --git a/modules/networking.js b/modules/networking.js index 270c541..b6e488d 100644 --- a/modules/networking.js +++ b/modules/networking.js @@ -4,16 +4,60 @@ var skins = require('./skins'); var fs = require("fs"); var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/"; +var skins_url = "https://skins.minecraft.net/MinecraftSkins/"; -var exp = {}; - -// download the Mojang profile for +uuid+ -// callback contains error, profile object -exp.get_profile = function(uuid, callback) { - if (uuid.length <= 16) { - callback(null, null); - return; +// 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; +} + +// 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", + timeout: config.http_timeout, + followRedirect: false + }, function(error, response, body) { + if (!error && response.statusCode == 301) { + // skin_url received successfully + console.log(name + " skin url received"); + callback(null, response.headers.location); + } else if (error) { + callback(error, null); + } else if (response.statusCode == 404) { + // skin doesn't exist + console.log(name + " has no skin"); + callback(0, null); + } else if (response.statusCode == 429) { + // Too Many Requests + // Never got this, seems like skins aren't limited + console.warn(name + " Too many requests"); + console.warn(body); + callback(null, null); + } else { + console.error(name + " Unknown error:"); + console.error(response); + console.error(body); + callback(null, 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, timeout: config.http_timeout // ms @@ -21,7 +65,7 @@ exp.get_profile = function(uuid, callback) { if (!error && response.statusCode == 200) { // profile downloaded successfully console.log(uuid + " profile downloaded"); - callback(null, JSON.parse(body)); + callback(null, extract_skin_url(JSON.parse(body))); } else if (error) { callback(error, null); } else if (response.statusCode == 204 || response.statusCode == 404) { @@ -42,6 +86,22 @@ exp.get_profile = function(uuid, callback) { }); }; +var exp = {}; + +// download skin_url for +uuid+ (name or uuid) +// callback contains error, skin_url +exp.get_skin_url = function(uuid, callback) { + if (uuid.length <= 16) { + get_username_url(uuid, function(err, url) { + callback(err, url); + }); + } else { + get_uuid_url(uuid, function(err, url) { + callback(err, url); + }); + } +}; + // downloads skin file from +url+ // stores face image as +facename+ // stores helm image as +helmname+ diff --git a/routes/avatars.js b/routes/avatars.js index 24d0bc3..888357f 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -6,8 +6,8 @@ var skins = require('../modules/skins'); var human_status = { 0: "none", 1: "cached", - 2: "checked", - 3: "downloaded", + 2: "downloaded", + 3: "checked", "-1": "error" }; diff --git a/test/test.js b/test/test.js index 949d1e7..032f42a 100644 --- a/test/test.js +++ b/test/test.js @@ -16,12 +16,12 @@ var usernames = fs.readFileSync('test/usernames.txt').toString().split("\n"); var uuid = uuids[Math.round(Math.random() * (uuids.length - 1))]; var username = usernames[Math.round(Math.random() * (usernames.length - 1))]; -describe('UUID/username', function() { +describe('Crafatar', function() { before(function() { cache.get_redis().flushall(); }); - describe('UUID', function() { + describe('UUID/username', function() { it("should be an invalid uuid", function(done) { assert.strictEqual(helpers.uuid_valid("g098cb60fa8e427cb299793cbd302c9a"), false); done(); @@ -58,43 +58,51 @@ describe('UUID/username', function() { assert.strictEqual(helpers.uuid_valid("a"), true); done(); }); - it("should not exist", function(done) { - networking.get_profile("00000000000000000000000000000000", function(err, profile) { + it("should not exist (uuid)", function(done) { + networking.get_skin_url("00000000000000000000000000000000", function(err, profile) { + assert.strictEqual(err, 0); + done(); + }); + }); + it("should not exist (username)", function(done) { + networking.get_skin_url("Steve", function(err, profile) { assert.strictEqual(err, 0); done(); }); }); }); - describe('Avatar', function() { + describe('Networking: Avatar', function() { it("should be downloaded (uuid)", function(done) { helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.strictEqual(status, 2); done(); }); }); - it("should be local (uuid)", function(done) { + it("should be cached (uuid)", function(done) { helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.strictEqual(status, 1); done(); }); }); + /* We can't test this because of mojang's rate limits :( it("should be checked (uuid)", function(done) { var original_cache_time = config.local_cache_time; config.local_cache_time = 0; helpers.get_avatar(uuid, false, 160, function(err, status, image) { - assert.strictEqual(status, 2); + assert.strictEqual(status, 3); config.local_cache_time = original_cache_time; done(); }); }); + */ it("should be downloaded (username)", function(done) { helpers.get_avatar(username, false, 160, function(err, status, image) { assert.strictEqual(status, 2); done(); }); }); - it("should be local (username)", function(done) { + it("should be cached (username)", function(done) { helpers.get_avatar(username, false, 160, function(err, status, image) { assert.strictEqual(status, 1); done(); @@ -104,7 +112,7 @@ describe('UUID/username', function() { var original_cache_time = config.local_cache_time; config.local_cache_time = 0; helpers.get_avatar(username, false, 160, function(err, status, image) { - assert.strictEqual(status, 2); + assert.strictEqual(status, 3); config.local_cache_time = original_cache_time; done(); }); @@ -137,18 +145,20 @@ describe('UUID/username', function() { }); }); it("should time out on profile download", function(done) { + var original_timeout = config.http_timeout; config.http_timeout = 1; - networking.get_profile("069a79f444e94726a5befca90e38aaf5", function(err, profile) { + networking.get_skin_url("069a79f444e94726a5befca90e38aaf5", function(err, profile) { assert.strictEqual(err.code, "ETIMEDOUT"); - config.http_timeout = 3000; + config.http_timeout = original_timeout; done(); }); }); it("should time out on skin download", function(done) { + var original_timeout = config.http_timeout; config.http_timeout = 1; networking.skin_file("http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", "face.png", "helm.png", function(err) { assert.strictEqual(err.code, "ETIMEDOUT"); - config.http_timeout = 3000; + config.http_timeout = original_timeout; done(); }); });