split code into more modules, fixes #8

This commit is contained in:
jomo 2014-10-29 19:52:36 +01:00
parent e2348bbb9d
commit cc159d3620
10 changed files with 230 additions and 176 deletions

8
modules/config.js Normal file
View File

@ -0,0 +1,8 @@
var config = {
min_size: 0, // < 0 will (obviously) cause crash
max_size: 512, // too big values might lead to slow response time or DoS
default_size: 180, // size to be used when no size given
browser_cache_time: 3600 // seconds until browser will request image again
};
module.exports = config;

72
modules/helpers.js Normal file
View File

@ -0,0 +1,72 @@
var networking = require('./networking');
var config = require('./config');
var skins = require('./skins');
var fs = require('fs');
var valid_uuid = /^[0-9a-f]{32}$/;
var skins_dir = config.skins_dir;
var exp = {};
// exracts the skin url of a +profile+ object
// returns null when no url found
exp.skin_url = function(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;
}
});
}
return url;
};
// returns true if the +uuid+ is a valid uuid
// the uuid may be not exist, however
exp.uuid_valid = function(uuid) {
return valid_uuid.test(uuid);
};
// handles requests for +uuid+ images with +size+
//
// callback is a function with 3 parameters:
// error, status, image buffer
//
// the status gives information about how the image was received
// -1: profile requested, but it was not found
// 1: found on disk
// 2: profile requested/found, skin downloaded from mojang servers
// 3: profile requested/found, but it has no skin
exp.get_avatar = function(uuid, size, callback) {
var filepath = skins_dir + uuid + ".png";
if (fs.existsSync(filepath)) {
skins.resize_img(filepath, size, function(result) {
callback(null, 1, result);
});
} else {
networking.get_profile(uuid, function(err, profile) {
if (err) {
callback(err, -1, profile);
}
var skinurl = exp.skin_url(profile);
if (skinurl) {
networking.skin_file(skinurl, filepath, function() {
console.log('got skin');
skins.resize_img(filepath, size, function(result) {
callback(null, 2, result);
});
});
} else {
// profile found, but has no skin
callback(null, 3, null);
}
});
}
};
module.exports = exp;

65
modules/networking.js Normal file
View File

@ -0,0 +1,65 @@
var request = require('request');
var skins = require('./skins');
var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
var exp = {};
exp.get_profile = function(uuid, callback) {
request.get({
url: session_url + uuid,
timeout: 1000 // ms
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
callback(null, JSON.parse(body));
} else {
if (error) {
callback(error, null);
return;
} 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)
} else if (response.statusCode == 429) {
// Too Many Requests
console.warn("Too many requests for " + uuid);
console.warn(body);
} else {
console.error("Unknown error:");
console.error(response);
console.error(body);
}
callback(null, null);
}
});
};
exp.skin_file = function(url, outname, callback) {
request.get({
url: url,
encoding: null, // encoding must be null so we get a buffer
timeout: 1000 // ms
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
skins.extract_face(body, outname, function() {
callback();
});
} else {
if (error) {
console.error(error);
} else if (response.statusCode == 404) {
console.warn("Texture not found: " + url);
} else if (response.statusCode == 429) {
// Too Many Requests
// Never got this, seems like textures aren't limited
console.warn("Too many requests for " + url);
console.warn(body);
} else {
console.error("Unknown error:");
console.error(response);
console.error(body);
}
callback(null);
}
});
};
module.exports = exp;

32
modules/skins.js Normal file
View File

@ -0,0 +1,32 @@
var lwip = require('lwip');
var exp = {};
// extracts the face from an image +buffer+
// save it to a file called +outname+
exp.extract_face = function(buffer, outname, callback) {
lwip.open(buffer, "png", function(err, image) {
if (err) throw err;
image.batch()
.crop(8, 8, 15, 15)
.writeFile(outname, function(err) {
if (err) throw err;
callback();
});
});
};
// resizes the image file +inname+ to +size+ by +size+ pixels
// +callback+ is a buffer of the resized image
exp.resize_img = function(inname, size, callback) {
lwip.open(inname, function(err, image) {
if (err) throw err;
image.batch()
.resize(size, size, "nearest-neighbor") // nearest-neighbor doesn't blur
.toBuffer('png', function(err, buffer) {
callback(buffer);
});
});
};
module.exports = exp;

View File

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www" "start": "node server.js"
}, },
"dependencies": { "dependencies": {
"express": "~4.9.0", "express": "~4.9.0",

View File

@ -1,78 +1,60 @@
var express = require('express'); var networking = require('../modules/networking');
var router = express.Router(); var helpers = require('../modules/helpers');
var skins = require('../skins'); var router = require('express').Router();
var config = require('../modules/config');
var skins = require('../modules/skins');
var fs = require('fs'); var fs = require('fs');
var valid_uuid = /^[0-9a-f]{32}$/; /* GET avatar request. */
/* GET home page. */
router.get('/:uuid/:size?', function(req, res) { router.get('/:uuid/:size?', function(req, res) {
var uuid = req.param('uuid'); var uuid = req.param('uuid');
var size = req.param('size') || 180; var size = req.param('size') || config.default_size;
var def = req.query.default; var def = req.query.default;
var start = new Date(); var start = new Date();
// Prevent app from crashing/freezing // Prevent app from crashing/freezing
if (size <= 0 || size > 512) size = 180; if (size <= config.min_size || size > config.max_size) {
if (valid_uuid.test(uuid)) { // "Unprocessable Entity", valid request, but semantically erroneous:
var filename = uuid + ".png"; // https://tools.ietf.org/html/rfc4918#page-78
if (fs.existsSync("skins/" + filename)) { res.status(422).send("422 Invalid size");
console.log('found ' + filename); return;
skins.resize_img("skins/" + filename, size, function(data) { } else if (!helpers.uuid_valid(uuid)) {
// tell browser to cache image locally for 10 minutes res.status(422).send("422 Invalid UUID");
var end = new Date() - start; return;
res.writeHead(200, {'Content-Type': 'image/png', 'Cache-Control': 'max-age=600, public', 'Response-Time': end, 'Storage-Type': 'local'}); }
res.end(data);
helpers.get_avatar(uuid, size, function(err, status, image) {
if (err) {
throw err;
} else if (status == 1 || status == 2) {
var time = new Date() - start;
sendimage(200, time, image);
} else if (status == 3) {
handle_404(def);
}
});
function handle_404(def) {
if (def == "alex" || def == "steve") {
skins.resize_img("public/images/" + def + ".png", size, function(image) {
var time = new Date() - start;
sendimage(404, time, image);
}); });
} else { } else {
console.log(filename + ' not found, downloading profile..'); res.status(404).send('404 Not found');
skins.get_profile(uuid, function(profile) {
var skinurl = skins.skin_url(profile);
if (skinurl) {
console.log('got profile, skin url is "' + skinurl + '" downloading..');
skins.skin_file(skinurl, "skins/" + filename, function() {
console.log('got skin');
skins.resize_img("skins/" + filename, size, function(data) {
// tell browser to cache image locally for 10 minutes
var end = new Date() - start;
res.writeHead(200, {
'Content-Type': 'image/png',
'Cache-Control': 'max-age=600, public',
'Response-Time': end,
'Storage-Type': 'downloaded'
});
res.end(data);
});
});
} else {
console.log('no skin url found');
switch (def) {
case "alex":
skins.resize_img("public/images/alex.png", size, function(data) {
// tell browser to cache image locally for 10 minutes
var end = new Date() - start;
res.writeHead(404, {'Content-Type': 'image/png', 'Cache-Control': 'max-age=600, public', 'Response-Time': end, 'Storage-Type': 'local'});
res.end(data);
});
break;
case "steve":
skins.resize_img("public/images/steve.png", size, function(data) {
// tell browser to cache image locally for 10 minutes
var end = new Date() - start;
res.writeHead(404, {'Content-Type': 'image/png', 'Cache-Control': 'max-age=600, public', 'Response-Time': end, 'Storage-Type': 'local'});
res.end(data);
});
break;
default:
res.status(404).send('404 Not found');
break;
}
}
});
} }
} else { }
res.status(422) // "Unprocessable Entity", valid request, but semantically erroneous: https://tools.ietf.org/html/rfc4918#page-78
.send("422 Invalid UUID"); function sendimage(status, time, image) {
res.writeHead(status, {
'Content-Type': 'image/png',
'Cache-Control': 'max-age=' + config.browser_cache_time + ', public',
'Response-Time': time,
'X-Storage-Type': 'local'
});
res.end(image);
} }
}); });
module.exports = router; module.exports = router;

View File

@ -1,9 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
var debug = require('debug')('crafatar'); var debug = require('debug')('crafatar');
var app = require('../app'); var app = require('./app');
app.set('port', process.env.PORT || 3000); app.set('port', process.env.PORT || 3000);
var server = app.listen(app.get('port'), function() { var server = app.listen(app.get('port'), function() {
debug('Express server listening on port ' + server.address().port); debug('Crafatar server listening on port ' + server.address().port);
}); });

105
skins.js
View File

@ -1,105 +0,0 @@
var request = require('request');
var lwip = require('lwip');
/*
* Skin retrieval methods are based on @jomo's CLI Crafatar implementation.
* https://github.com/jomo/Crafatar
*/
function extract_face(buffer, outname, callback) {
lwip.open(buffer, "png", function(err, image) {
if (err) {
console.log('c ' + buffer.length);
throw err;
}
image.batch()
.crop(8, 8, 15, 15)
.writeFile(outname, function(err) {
if (err) throw err;
callback();
});
});
}
module.exports = {
get_profile: function(uuid, callback) {
request.get({
url: "https://sessionserver.mojang.com/session/minecraft/profile/" + uuid,
timeout: 1000 // ms
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
callback(JSON.parse(body));
} else {
if (error) {
console.error(error);
} 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)
} else if (response.statusCode == 429) {
// Too Many Requests
console.warn("Too many requests for " + uuid);
console.warn(body);
} else {
console.error("Unknown error:");
console.error(response);
console.error(body);
}
callback(null);
}
});
},
skin_url: function(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;
}
});
}
return url;
},
skin_file: function(url, outname, callback) {
request.get({
url: url,
encoding: null, // encoding must be null so we get a buffer
timeout: 1000 // ms
}, function (error, response, body) {
if (!error && response.statusCode == 200) {
extract_face(body, outname, function() {
callback();
});
} else {
if (error) {
console.error(error);
} else if (response.statusCode == 404) {
console.warn("Texture not found: " + url);
} else if (response.statusCode == 429) {
// Too Many Requests
// Never got this, seems like textures aren't limited
console.warn("Too many requests for " + url);
console.warn(body);
} else {
console.error("Unknown error:");
console.error(response);
console.error(body);
}
callback(null);
}
});
},
resize_img: function(inname, size, callback) {
lwip.open(inname, function(err, image) {
if (err) throw err;
image.batch()
.resize(size, size, "nearest-neighbor") // nearest-neighbor doesn't blur
.toBuffer('png', function(err, buffer) {
callback(buffer);
});
});
}
};