From dd7e46f377867488e97e30da54def96108f37c2d Mon Sep 17 00:00:00 2001 From: Navarr Barnier Date: Fri, 12 Dec 2014 10:16:40 -0500 Subject: [PATCH] Implement Skeleton for Returning Capes (Currently returns skins) Align capes.js codestyle to the rest of the project get_cape_hash helper (not working - wip) clean up capes.js --- app.js | 63 +++++++++++++++ modules/cache.js | 2 +- modules/config.example.js | 3 +- modules/helpers.js | 73 +++++++++++++++++- modules/networking.js | 62 ++++++++++++++- routes/capes.js | 66 ++++++++++++++++ skins/capes/.gitkeep | 0 ...b50a3a288f9c589762b3dae8308842122dcb81.png | Bin 0 -> 907 bytes ...62631978253979db673c2fbcc27dc3d2dcc7a7.png | Bin 0 -> 610 bytes 9 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 app.js create mode 100644 routes/capes.js create mode 100644 skins/capes/.gitkeep create mode 100644 skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png create mode 100644 skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png 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 0000000000000000000000000000000000000000..a6b7b0ba353d9929a81453dc41717a28143e6231 GIT binary patch literal 907 zcmV;619bd}P)|=YLRP^i(MA3?d4+^TA>!bB2tfov$GG>? z84|Y*X#&#>Gu1s+UH9C3&Z#R)Den(AyRI|V~$%w4WfwaGb4 z1p>*F*B?Jk2*AweSK3&mrP56iohXu6t+O!%WppW>lv+sfnO#)1rz0%PK)0^WO^bXYspHV_#x|UB zjrXgoJCCaj$a={&$3zTYQcT`8{Q+mtKui_Pw3lLgFbqM|$Oh940dhZGe^RkY1#1X5 zgusNW>)%Z7#h|pF_z)lx07zsy062K@IR**>sRHNM^H<-%Mw;mzIC`zMc&M`;HN$=! zlv@-stexuHKwg@;=kv__ReBpbR{>Wj)H>kQT4IT#l6Cm9$?K9xqh zd$a#rnyn3*yi!&EE72VhYcye!BQ;DFJTA3v7#iq~)ql}!js1{cAn%1F>G^b1g z8&7b1#sgarLsfR|k*WsT&^y%PeRn>f0<=1*b`xE-n#%9KwI+1VU`&Y$(6(unjjj}; zUOlAho>aiQhj$+Htqq|#G=Rv}6JZE0B$W;GR=L`2qF+u4Dct%4KlFNaj#>d*FNuly z0~9%E@fN6ibu+PtjL+2)!1x@Fd&9B<z`~{K2vbE;ke$D^@002ovPDHLkV1i#4tl$6u literal 0 HcmV?d00001 diff --git a/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png b/skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png new file mode 100644 index 0000000000000000000000000000000000000000..8933fddb8e287837d1ee739bdcab18b77539d452 GIT binary patch literal 610 zcmV-o0-gPdP)L9M=YJ)J+OP{J{IPjxigzhCw?I!9C90LHZSGpzk zT6{(<(vtXxfrv4i{z}cJDJDq13jrVm1Ec}YS-V$MD^)oCt9`n>(?WoDumI_}upl}y zHR2#vE5Eh~<0#PFkpe_}ItZ)FK+7|DTl9@5lv{*rb+m1mbhpt@UZf!mh&ShSB>CH% zWe1>OYh{fdwpV5C;QX?*&Hm#05>>A6W!!y>_Qt6G^6n06;oAb_^Le^EKai779_6KV z_&6kd2wQ-|c!4OJ{-LeUaM zB*>aWF@gwqEi$}B(3LcEg0;6m^!SYu<$n6K-e^-KQmIku-mX%`vM4J2`g9lSeFxbA z%*r%PK|Dp-Y?d$vFm}TaE6Mn*u01$MGz+!ljcgJ)TL!JTn z#aAp|iK|G4VUQ*EJphXskeE(O%Qqd#^4y!PeF$c9b2O0o`TBc2E7{{QQ4Rn8