diff --git a/.travis.yml b/.travis.yml index 79c7d95..e143f58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,5 @@ notifications: - "irc.esper.net#spongy" skip_join: true services: - - redis-server \ No newline at end of file + - redis-server +skip_join: true diff --git a/README.md b/README.md index 128a443..61ca922 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -# Crafatar +# Crafatar [![travis](https://api.travis-ci.org/Jake0oo0/Spongy.svg)](https://travis-ci.org/Jake0oo0/Spongy/) -Crafatar serves Minecraft skins and heads for use in external applications. +https://crafatar.com + +Crafatar serves Minecraft avatars based on the skin for use in external applications. Inspired by [Gravatar](https://gravatar.com) (hence the name) and [Minotar](https://minotar.net). +Image manipulation is done by [lwip](https://github.com/EyalAr/lwip) + ## Usage See the [API Usage](https://crafatar.com) diff --git a/modules/cache.js b/modules/cache.js index b350b8f..fa09bcc 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -1,19 +1,20 @@ var config = require("./config"); var redis = null; +var fs = require("fs"); function connect_redis() { - console.log("connecting to redis"); + console.log("connecting to redis..."); if (process.env.REDISCLOUD_URL) { var redisURL = require("url").parse(process.env.REDISCLOUD_URL); redis = require("redis").createClient(redisURL.port, redisURL.hostname); redis.auth(redisURL.auth.split(":")[1]); - redis.flushall(); } else { redis = require("redis").createClient(); } redis.on("ready", function() { - console.log("Redis connection established."); + console.log("Redis connection established. Flushing all data."); + redis.flushall(); }); redis.on("error", function (err) { console.error(err); @@ -23,17 +24,37 @@ function connect_redis() { }); } +// sets the date of the face file belonging to +hash+ to now +function update_file_date(hash) { + if (hash) { + var path = config.faces_dir + hash + ".png"; + fs.exists(path, function(exists) { + if (exists) { + var date = new Date(); + fs.utimes(path, date, date, function(err){ + if (err) { + console.error(err); + } + }); + } else { + console.error("Tried to update " + path + " date, but it doesn't exist"); + } + }); + } +} + var exp = {}; exp.get_redis = function() { return redis; }; -// sets the timestamp for +uuid+ to now -exp.update_timestamp = function(uuid) { +// sets the timestamp for +uuid+ and its face file's date to now +exp.update_timestamp = function(uuid, hash) { console.log(uuid + " cache: updating timestamp"); var time = new Date().getTime(); redis.hmset(uuid, "t", time); + update_file_date(hash); }; // create the key +uuid+, store +hash+ and time @@ -52,7 +73,7 @@ exp.get_details = function(uuid, callback) { if (data) { details = { hash: (data.h == "null" ? null : data.h), - time: data.t + time: Number(data.t) }; } callback(err, details); diff --git a/modules/config.js b/modules/config.js index c9b6725..9b15725 100644 --- a/modules/config.js +++ b/modules/config.js @@ -1,8 +1,8 @@ var config = { - min_size: 0, // < 0 will (obviously) cause crash + min_size: 1, // < 1 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 - local_cache_time: 3600, // seconds until we will check if the image changed + 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 faces_dir: 'skins/faces/', // directory where faces are kept. should have trailing '/' diff --git a/modules/helpers.js b/modules/helpers.js index 815c152..d9493ad 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -30,7 +30,7 @@ function store_images(uuid, details, callback) { if (details && details.hash == hash) { // hash hasn't changed console.log(uuid + " hash has not changed"); - cache.update_timestamp(uuid); + cache.update_timestamp(uuid, hash); callback(null, hash); } else { // hash has changed @@ -83,7 +83,7 @@ function get_image_hash(uuid, callback) { if (err) { callback(err, -1, null); } else { - if (details && details.time + config.local_cache_time >= new Date().getTime()) { + if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) { // uuid known + recently updated console.log(uuid + " uuid known & recently updated"); callback(null, (details.hash ? 1 : 0), details.hash); diff --git a/modules/skins.js b/modules/skins.js index 774ffe3..d4f94e2 100644 --- a/modules/skins.js +++ b/modules/skins.js @@ -78,4 +78,13 @@ exp.resize_img = function(inname, size, callback) { }); }; +// returns "alex" or "steve" calculated by the +uuid+ +exp.default_skin = function(uuid) { + if (Number("0x" + uuid[31]) % 2 === 0) { + return "alex"; + } else { + return "steve"; + } +}; + module.exports = exp; \ No newline at end of file diff --git a/public/images/alex.png b/public/images/alex.png index b1222ed..8048d1c 100644 Binary files a/public/images/alex.png and b/public/images/alex.png differ diff --git a/public/images/steve.png b/public/images/steve.png index c34399a..de4d1ce 100644 Binary files a/public/images/steve.png and b/public/images/steve.png differ diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index 026479a..c37700f 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -26,38 +26,39 @@ mark.green { .code { font-family: monospace; + word-wrap: break-word; } .sideface { - width: 180px; - height: 180px; + width: 160px; + height: 160px; } .sideface.Jake0oo0 { - background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=180&default=alex"); + background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=160"); } .sideface.Jake0oo0:hover { - background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=180&default=alex&helm=true"); + background:url("/avatars/2d5aa9cdaeb049189930461fc9b91cc5?size=160&helm=true"); } .sideface.redstone_sheep { - background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=180&default=alex"); + background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=160"); } .sideface.redstone_sheep:hover { - background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=180&default=alex&helm=true"); + background:url("/avatars/ae795aa86327408e92ab25c8a59f3ba1?size=160&helm=true"); } .sideface.Notch { - background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=180&default=alex"); + background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=160"); } .sideface.Notch:hover { - background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=180&default=alex&helm=true"); + background:url("/avatars/069a79f444e94726a5befca90e38aaf5?size=160&helm=true"); } .sideface.sk89q { - background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=180&default=alex"); + background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=160"); } .sideface.sk89q:hover { - background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=180&default=alex&helm=true"); + background:url("/avatars/0ea8eca3dbf647cc9d1ac64551ca975c?size=160&helm=true"); } .sideface.md_5 { - background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=180&default=alex"); + background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=160"); } .sideface.md_5:hover { - background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=180&default=alex&helm=true"); + background:url("/avatars/af74a02d19cb445bb07f6866a861f783?size=160&helm=true"); } \ No newline at end of file diff --git a/routes/avatars.js b/routes/avatars.js index b486e2e..af29152 100644 --- a/routes/avatars.js +++ b/routes/avatars.js @@ -12,7 +12,7 @@ router.get('/:uuid.:ext?', function(req, res) { var start = new Date(); // Prevent app from crashing/freezing - if (size <= config.min_size || size > config.max_size) { + if (size < config.min_size || size > config.max_size) { // "Unprocessable Entity", valid request, but semantically erroneous: // https://tools.ietf.org/html/rfc4918#page-78 res.status(422).send("422 Invalid size"); @@ -29,35 +29,34 @@ router.get('/:uuid.:ext?', function(req, res) { console.error(err); if (image) { console.warn("error occured, image found anyway"); - sendimage(200, status, image); + sendimage(503, true, image); } else { - handle_404(def); + handle_default(404); } } else if (status == 1 || status == 2) { sendimage(200, status == 1, image); - } else if (status == 0 || status == 3) { - handle_404(def); + } else if (status === 0 || status == 3) { + handle_default(404); } else { console.error("unexpected error/status"); console.error("error: " + err); console.error("status: " + status); - handle_404(def); + handle_default(404); } }); } catch(e) { console.error("Error!"); console.error(e); - res.status(500).send("500 Internal server error"); + handle_default(500); } - function handle_404(def) { - if (def == "alex" || def == "steve") { - skins.resize_img("public/images/" + def + ".png", size, function(err, image) { - sendimage(404, true, image); - }); - } else { - res.status(404).send('404 Not found'); + function handle_default(status) { + if (def != "steve" && def != "alex") { + def = skins.default_skin(uuid); } + skins.resize_img("public/images/" + def + ".png", size, function(err, image) { + sendimage(status, true, image); + }); } function sendimage(status, local, image) { diff --git a/routes/index.js b/routes/index.js index 79f08f6..ee6e85e 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,9 +5,7 @@ var router = express.Router(); router.get('/', function(req, res) { res.render('index', { title: 'Crafatar', - domain: "https://" + req.headers.host, - // see http://stackoverflow.com/a/14924922/2517068 - commit: process.env.HEAD_HASH || "unknown" + domain: "https://" + req.headers.host }); }); diff --git a/test/bulk.sh b/test/bulk.sh index 6b7feed..5267027 100755 --- a/test/bulk.sh +++ b/test/bulk.sh @@ -1,4 +1,9 @@ #!/bin/bash +host="$1" +if [ -z "$host" ]; then + echo "Usage: $0 " + exit 1 +fi dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" rm -f "$dir/../skins/"*.png || exit 1 for uuid in `cat "$dir/uuids.txt"`; do @@ -8,5 +13,5 @@ for uuid in `cat "$dir/uuids.txt"`; do if [ "$(( ((RANDOM<<15)|RANDOM) % 2 ))" -eq "1" ]; then helm="&helm" fi - curl -sS -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://crafatar.com/avatars/$uuid?size=$size$helm" || exit 1 + curl -sSL -o /dev/null -w "%{url_effective} %{http_code} %{time_total}s\\n" "http://$host/avatars/$uuid?size=$size$helm" || exit 1 done diff --git a/test/test.js b/test/test.js index 7c226f1..712196f 100644 --- a/test/test.js +++ b/test/test.js @@ -27,13 +27,13 @@ describe('Avatar Serving', function(){ }); describe('Avatar', function(){ it("should be downloaded", function(done) { - helpers.get_avatar(uuid, false, 180, function(err, status, image) { + helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.equal(status, 2); done(); }); }); it("should be local", function(done) { - helpers.get_avatar(uuid, false, 180, function(err, status, image) { + helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.equal(status, 1); done(); }); @@ -44,7 +44,7 @@ describe('Avatar Serving', function(){ cache.get_redis().flushall(); }); it("should be rate limited", function(done) { - helpers.get_avatar(uuid, false, 180, function(err, status, image) { + helpers.get_avatar(uuid, false, 160, function(err, status, image) { assert.equal(err, null); done(); }); diff --git a/views/index.jade b/views/index.jade index 0febd7f..b13bff5 100644 --- a/views/index.jade +++ b/views/index.jade @@ -23,13 +23,13 @@ block content h3 Parameters h4 size - p The size of the image in pixels, 1 - 512.
Default is 180. + p The size of the image in pixels, 1 - 512.
Default is 160. h4 default p The image to be returned when the uuid has no skin.
Valid options are a(href="/avatars/00000000000000000000000000000000?default=steve") steve | or a(href="/avatars/00000000000000000000000000000000?default=alex") alex - | .
Otherwise, a 404 with no content is returned. + | .
The default is calculated based on the UUID (even = alex, odd = steve) h4 helm p Get an avatar with the second (helmet) layer applied.
The content of this parameter is ignored @@ -41,7 +41,7 @@ block content p Either 'local' or 'downloaded'. Local means that Crafatar already had the image on disk, while downloaded means that it was retrieved from Mojang's skin servers. h3 Examples - p Get jeb_'s avatar, 180 × 180 pixels + p Get jeb_'s avatar, 160 × 160 pixels img(src="/avatars/853c80ef3c3749fdaa49938b674adae6") .well.code <img src="#{domain}/avatars/853c80ef3c3749fdaa49938b674adae6"> p Get jeb_'s avatar, 64 × 64 pixels @@ -58,7 +58,4 @@ block content .sideface.Jake0oo0(title="Jake0oo0") .sideface.Notch(title="Notch") .sideface.sk89q(title="sk89q") - .sideface.md_5(title="md_5") - hr - small Site version - a(href="https://github.com/Jake0oo0/crafatar/commit/#{commit}") #{commit} \ No newline at end of file + .sideface.md_5(title="md_5") \ No newline at end of file diff --git a/views/layout.jade b/views/layout.jade index 240b8dd..de5fcf7 100644 --- a/views/layout.jade +++ b/views/layout.jade @@ -5,6 +5,7 @@ html link(rel='stylesheet', href='/stylesheets/style.css') link(rel="icon", type="image/x-icon", href="/favicon.ico") link(href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css", rel="stylesheet") + meta(name="viewport" content="initial-scale=1,maximum-scale=1") script(src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js") body a.forkme(href="https://github.com/Jake0oo0/crafatar", target="_blank") @@ -17,8 +18,4 @@ html span.icon-bar span.icon-bar a.navbar-brand(href='/') Crafatar - .navbar-collapse.collapse - ul.nav.navbar-nav - li.active - a(href='/') Home block content \ No newline at end of file