networking.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. var http_code = require("http").STATUS_CODES;
  2. var logging = require("./logging");
  3. var request = require("request");
  4. var config = require("../config");
  5. var skins = require("./skins");
  6. require("./object-patch");
  7. var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
  8. var skins_url = "https://skins.minecraft.net/MinecraftSkins/";
  9. var capes_url = "https://skins.minecraft.net/MinecraftCloaks/";
  10. var textures_url = "http://textures.minecraft.net/texture/";
  11. var mojang_urls = [skins_url, capes_url];
  12. var exp = {};
  13. // extracts the +type+ [SKIN|CAPE] URL
  14. // from the nested & encoded +profile+ object
  15. // returns the URL or null if not present
  16. function extract_url(profile, type) {
  17. var url = null;
  18. if (profile && profile.properties) {
  19. profile.properties.forEach(function(prop) {
  20. if (prop.name === "textures") {
  21. var json = new Buffer(prop.value, "base64").toString();
  22. var props = JSON.parse(json);
  23. url = Object.get(props, "textures." + type + ".url") || null;
  24. }
  25. });
  26. }
  27. return url;
  28. }
  29. // extracts the +type+ [SKIN|CAPE] URL
  30. // from the nested & encoded +profile+ object
  31. // returns the if the model is "slim"
  32. function extract_model(profile) {
  33. var slim = null;
  34. if (profile && profile.properties) {
  35. profile.properties.forEach(function(prop) {
  36. if (prop.name === "textures") {
  37. var json = new Buffer(prop.value, "base64").toString();
  38. var props = JSON.parse(json);
  39. slim = Object.get(props, "textures.SKIN.metadata.model");
  40. }
  41. });
  42. }
  43. return slim === "slim";
  44. }
  45. // helper method that calls `get_username_url` or `get_uuid_info` based on the +usedId+
  46. // +userId+ is used for usernames, while +profile+ is used for UUIDs
  47. // callback: error, url, slim
  48. function get_info(rid, userId, profile, type, callback) {
  49. if (userId.length <= 16) {
  50. // username
  51. exp.get_username_url(rid, userId, type, function(err, url) {
  52. callback(err, url || null, false);
  53. });
  54. } else {
  55. exp.get_uuid_info(profile, type, function(url, slim) {
  56. callback(null, url || null, slim);
  57. });
  58. }
  59. }
  60. // performs a GET request to the +url+
  61. // +options+ object includes these options:
  62. // encoding (string), default is to return a buffer
  63. // callback: the body, response,
  64. // and error buffer. get_from helper method is available
  65. exp.get_from_options = function(rid, url, options, callback) {
  66. request.get({
  67. url: url,
  68. headers: {
  69. "User-Agent": "https://crafatar.com"
  70. },
  71. timeout: config.server.http_timeout,
  72. followRedirect: false,
  73. encoding: options.encoding || null,
  74. }, function(error, response, body) {
  75. // log url + code + description
  76. var code = response && response.statusCode;
  77. var logfunc = code && code < 405 ? logging.debug : logging.warn;
  78. logfunc(rid, url, code || error && error.code, http_code[code]);
  79. // not necessarily used
  80. var e = new Error(code);
  81. e.name = "HTTP";
  82. e.code = "HTTPERROR";
  83. switch (code) {
  84. case 200:
  85. case 301:
  86. case 302: // never seen, but mojang might use it in future
  87. case 307: // never seen, but mojang might use it in future
  88. case 308: // never seen, but mojang might use it in future
  89. // these are okay
  90. break;
  91. case 204: // no content, used like 404 by mojang. making sure it really has no content
  92. case 404:
  93. // can be cached as null
  94. body = null;
  95. break;
  96. case 429: // this shouldn't usually happen, but occasionally does
  97. case 500:
  98. case 503:
  99. case 504:
  100. // we don't want to cache this
  101. error = error || e;
  102. body = null;
  103. break;
  104. default:
  105. if (!error) {
  106. // Probably 500 or the likes
  107. logging.error(rid, "Unexpected response:", code, body);
  108. }
  109. error = error || e;
  110. body = null;
  111. break;
  112. }
  113. if (body && !body.length) {
  114. // empty response
  115. body = null;
  116. }
  117. callback(body, response, error);
  118. });
  119. };
  120. // helper method for get_from_options, no options required
  121. exp.get_from = function(rid, url, callback) {
  122. exp.get_from_options(rid, url, {}, function(body, response, err) {
  123. callback(body, response, err);
  124. });
  125. };
  126. // make a request to skins.miencraft.net
  127. // the skin url is taken from the HTTP redirect
  128. // type reference is above
  129. exp.get_username_url = function(rid, name, type, callback) {
  130. type = Number(type === "CAPE");
  131. exp.get_from(rid, mojang_urls[type] + name + ".png", function(body, response, err) {
  132. if (!err) {
  133. if (response) {
  134. callback(err, response.statusCode === 404 ? null : response.headers.location);
  135. } else {
  136. callback(err, null);
  137. }
  138. } else {
  139. callback(err, null);
  140. }
  141. });
  142. };
  143. // gets the URL for a skin/cape from the profile
  144. // +type+ "SKIN" or "CAPE", specifies which to retrieve
  145. // callback: url, slim
  146. exp.get_uuid_info = function(profile, type, callback) {
  147. var properties = Object.get(profile, "properties") || [];
  148. properties.forEach(function(prop) {
  149. if (prop.name === "textures") {
  150. var json = new Buffer(prop.value, "base64").toString();
  151. profile = JSON.parse(json);
  152. }
  153. });
  154. var url = Object.get(profile, "textures." + type + ".url");
  155. var slim;
  156. if (type === "SKIN") {
  157. slim = Object.get(profile, "textures.SKIN.metadata.model");
  158. }
  159. callback(url || null, !!slim);
  160. };
  161. // make a request to sessionserver for +uuid+
  162. // callback: error, profile
  163. exp.get_profile = function(rid, uuid, callback) {
  164. if (!uuid) {
  165. callback(null, null);
  166. } else {
  167. exp.get_from_options(rid, session_url + uuid, { encoding: "utf8" }, function(body, response, err) {
  168. try {
  169. body = body ? JSON.parse(body) : null;
  170. callback(err || null, body);
  171. } catch(e) {
  172. if (e instanceof SyntaxError) {
  173. logging.warn(rid, "Failed to parse JSON", e);
  174. logging.debug(rid, body);
  175. callback(err || null, null);
  176. } else {
  177. throw e;
  178. }
  179. }
  180. });
  181. }
  182. };
  183. // get the skin URL and type for +userId+
  184. // +profile+ is used if +userId+ is a uuid
  185. // callback: error, url, slim
  186. exp.get_skin_info = function(rid, userId, profile, callback) {
  187. get_info(rid, userId, profile, "SKIN", callback);
  188. };
  189. // get the cape URL for +userId+
  190. // +profile+ is used if +userId+ is a uuid
  191. exp.get_cape_url = function(rid, userId, profile, callback) {
  192. get_info(rid, userId, profile, "CAPE", callback);
  193. };
  194. // download the +tex_hash+ image from the texture server
  195. // and save it in the +outpath+ file
  196. // callback: error, response, image buffer
  197. exp.save_texture = function(rid, tex_hash, outpath, callback) {
  198. if (tex_hash) {
  199. var textureurl = textures_url + tex_hash;
  200. exp.get_from(rid, textureurl, function(img, response, err) {
  201. if (err) {
  202. callback(err, response, null);
  203. } else {
  204. skins.save_image(img, outpath, function(img_err, saved_img) {
  205. callback(img_err, response, saved_img);
  206. });
  207. }
  208. });
  209. } else {
  210. callback(null, null, null);
  211. }
  212. };
  213. module.exports = exp;