浏览代码

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
Navarr Barnier 10 年之前
父节点
当前提交
dd7e46f377

+ 63 - 0
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;

+ 1 - 1
modules/cache.js

@@ -137,4 +137,4 @@ exp.get_details = function(uuid, callback) {
 };
 
 connect_redis();
-module.exports = exp;
+module.exports = exp;

+ 2 - 1
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;
+module.exports = config;

+ 72 - 1
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;
+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;

+ 61 - 1
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;
+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;

+ 66 - 0
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;

+ 0 - 0
skins/capes/.gitkeep


二进制
skins/capes/3f688e0e699b3d9fe448b5bb50a3a288f9c589762b3dae8308842122dcb81.png


二进制
skins/capes/95a2d2d94942966f743b84e4c262631978253979db673c2fbcc27dc3d2dcc7a7.png