diff --git a/app.js b/app.js new file mode 100644 index 0000000..8fcd215 --- /dev/null +++ b/app.js @@ -0,0 +1,63 @@ +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; diff --git a/modules/cache.js b/modules/cache.js index ba86d44..fd96122 100644 --- a/modules/cache.js +++ b/modules/cache.js @@ -137,4 +137,4 @@ exp.get_details = function(uuid, callback) { }; connect_redis(); -module.exports = exp; \ No newline at end of file +module.exports = exp; diff --git a/modules/config.example.js b/modules/config.example.js index fa23a8f..651d371 100644 --- a/modules/config.example.js +++ b/modules/config.example.js @@ -13,10 +13,11 @@ var config = { helms_dir: "images/helms/", // directory where helms 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 "/" + 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; \ No newline at end of file +module.exports = config; diff --git a/modules/helpers.js b/modules/helpers.js index 68bc4fc..fe2f3ba 100644 --- a/modules/helpers.js +++ b/modules/helpers.js @@ -75,6 +75,45 @@ function store_images(uuid, details, callback) { } } }); + + 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); + } else { + // download cape + networking.get_cape(cape_url, function(err, img) { + if (err || !img) { + callback(err, null); + } + }); + } + } + } else { + // profile found, but has no cape + cache.save_hash(uuid, null); + callback(null, null); + } + } + }); } @@ -127,6 +166,19 @@ exp.get_image_hash = function(uuid, callback) { }); }; +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"); + + } + } + }) +}; + // handles requests for +uuid+ avatars with +size+ // callback contains error, status, image buffer, hash @@ -224,4 +276,23 @@ exp.get_render = function(uuid, scale, helm, body, callback) { }); }; -module.exports = exp; \ No newline at end of file +exp.get_cape = function(uuid, callback) { + logging.log(uuid + " cape request"); + exp.get_image_hash(uuid, function(err, status, 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); + } + }); +}; + +module.exports = exp; diff --git a/modules/networking.js b/modules/networking.js index d1067c4..9f36e92 100644 --- a/modules/networking.js +++ b/modules/networking.js @@ -6,6 +6,7 @@ var fs = require("fs"); var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/"; var skins_url = "https://skins.minecraft.net/MinecraftSkins/"; +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) @@ -23,6 +24,20 @@ function extract_skin_url(profile) { 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) { @@ -104,6 +119,18 @@ exp.get_skin_url = function(uuid, callback) { } }; +exp.get_cape_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+ // callback contains error, image exp.get_skin = function(url, uuid, callback) { @@ -162,4 +189,37 @@ exp.save_skin = function(uuid, hash, outpath, callback) { } }; -module.exports = exp; \ No newline at end of file +exp.get_cape = function(url, callback) { + request.get({ + url: url, + 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); + } + }); +}; + +module.exports = exp; diff --git a/routes/capes.js b/routes/capes.js new file mode 100644 index 0000000..80e47d5 --- /dev/null +++ b/routes/capes.js @@ -0,0 +1,66 @@ +var logging = require("../modules/logging"); +var helpers = require("../modules/helpers"); +var config = require("../modules/config"); +var router = require("express").Router(); +var lwip = require("lwip"); + +/* GET skin request. */ +router.get("/:uuid.:ext?", function (req, res) { + var uuid = (req.params.uuid || ""); + var def = req.query.default; + var start = new Date(); + var etag = null; + + if (!helpers.uuid_valid(uuid)) { + res.status(422).send("422 Invalid UUID"); + return; + } + + // strip dashes + uuid = uuid.replace(/-/g, ""); + + try { + helpers.get_cape(uuid, function (err, hash, image) { + logging.log(uuid); + if (err) { + logging.error(err); + } + etag = hash && hash.substr(0, 32) || "none"; + var matches = req.get("If-None-Match") == "\"" + etag + "\""; + if (image) { + var http_status = 200; + if (matches) { + http_status = 304; + } else if (err) { + http_status = 503; + } + logging.debug("Etag: " + req.get("If-None-Match")); + logging.debug("matches: " + matches); + logging.log("status: " + http_status); + sendimage(http_status, image); + } else { + res.status(404).send("404 not found"); + } + }); + } catch (e) { + logging.error("Error!"); + logging.error(e); + res.status(500).send("500 error while retrieving cape"); + } + + + function sendimage(http_status, image) { + res.writeHead(http_status, { + "Content-Type": "image/png", + "Cache-Control": "max-age=" + config.browser_cache_time + ", public", + "Response-Time": new Date() - start, + "X-Storage-Type": "downloaded", + "Access-Control-Allow-Origin": "*", + "Etag": "\"" + etag + "\"" + }); + res.end(http_status == 304 ? null : image); + } +}); + + +module.exports = router; diff --git a/skins/capes/.gitkeep b/skins/capes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png b/skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png new file mode 100644 index 0000000..a6b7b0b Binary files /dev/null and b/skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png differ diff --git a/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png b/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png new file mode 100644 index 0000000..8933fdd Binary files /dev/null and b/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png differ