From cc159d362029293ad5a9b2996808afb99f559f6f Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 29 Oct 2014 19:52:36 +0100 Subject: [PATCH] split code into more modules, fixes #8 --- app.js | 2 +- modules/config.js | 8 +++ modules/helpers.js | 72 +++++++++++++++++++++++++++ modules/networking.js | 65 ++++++++++++++++++++++++ modules/skins.js | 32 ++++++++++++ package.json | 2 +- routes/avatars.js | 112 ++++++++++++++++++------------------------ routes/index.js | 2 +- bin/www => server.js | 6 +-- skins.js | 105 --------------------------------------- 10 files changed, 230 insertions(+), 176 deletions(-) create mode 100644 modules/config.js create mode 100644 modules/helpers.js create mode 100644 modules/networking.js create mode 100644 modules/skins.js rename bin/www => server.js (60%) delete mode 100644 skins.js diff --git a/app.js b/app.js index d167ace..a37c962 100644 --- a/app.js +++ b/app.js @@ -56,4 +56,4 @@ app.use(function(err, req, res, next) { }); }); -module.exports = app; +module.exports = app; \ No newline at end of file diff --git a/modules/config.js b/modules/config.js new file mode 100644 index 0000000..ec0d945 --- /dev/null +++ b/modules/config.js @@ -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; \ No newline at end of file diff --git a/modules/helpers.js b/modules/helpers.js new file mode 100644 index 0000000..367a5f1 --- /dev/null +++ b/modules/helpers.js @@ -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; \ No newline at end of file diff --git a/modules/networking.js b/modules/networking.js new file mode 100644 index 0000000..a0c5c27 --- /dev/null +++ b/modules/networking.js @@ -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; \ No newline at end of file diff --git a/modules/skins.js b/modules/skins.js new file mode 100644 index 0000000..1e99c8d --- /dev/null +++ b/modules/skins.js @@ -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; \ No newline at end of file diff --git a/package.json b/package.json index 4461329..5e43530 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "node ./bin/www" + "start": "node server.js" }, "dependencies": { "express": "~4.9.0", diff --git a/routes/avatars.js b/routes/avatars.js index b37fcee..9f98ced 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -1,78 +1,60 @@ -var express = require('express'); -var router = express.Router(); -var skins = require('../skins'); +var networking = require('../modules/networking'); +var helpers = require('../modules/helpers'); +var router = require('express').Router(); +var config = require('../modules/config'); +var skins = require('../modules/skins'); var fs = require('fs'); -var valid_uuid = /^[0-9a-f]{32}$/; - -/* GET home page. */ +/* GET avatar request. */ router.get('/:uuid/:size?', function(req, res) { 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 start = new Date(); + // Prevent app from crashing/freezing - if (size <= 0 || size > 512) size = 180; - if (valid_uuid.test(uuid)) { - var filename = uuid + ".png"; - if (fs.existsSync("skins/" + filename)) { - console.log('found ' + filename); - 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': 'local'}); - res.end(data); + if (size <= config.min_size || size > config.max_size) { + // "Unprocessable Entity", valid request, but semantically erroneous: + // https://tools.ietf.org/html/rfc4918#page-78 + res.status(422).send("422 Invalid size"); + return; + } else if (!helpers.uuid_valid(uuid)) { + res.status(422).send("422 Invalid UUID"); + return; + } + + 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 { - console.log(filename + ' not found, downloading profile..'); - 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; - } - } - }); + res.status(404).send('404 Not found'); } - } 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; \ No newline at end of file diff --git a/routes/index.js b/routes/index.js index 30045e6..81d891e 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,4 +7,4 @@ router.get('/', function(req, res) { }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/bin/www b/server.js similarity index 60% rename from bin/www rename to server.js index 89a732e..bbb2ef0 100644 --- a/bin/www +++ b/server.js @@ -1,9 +1,9 @@ #!/usr/bin/env node var debug = require('debug')('crafatar'); -var app = require('../app'); +var app = require('./app'); app.set('port', process.env.PORT || 3000); 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); +}); \ No newline at end of file diff --git a/skins.js b/skins.js deleted file mode 100644 index 1c4464f..0000000 --- a/skins.js +++ /dev/null @@ -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); - }); - }); - } -}; \ No newline at end of file