2
0

networking.js 6.4 KB

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