networking.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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 textures_url = "https://textures.minecraft.net/texture/";
  9. var exp = {};
  10. // performs a GET request to the +url+
  11. // +options+ object includes these options:
  12. // encoding (string), default is to return a buffer
  13. // callback: the body, response,
  14. // and error buffer. get_from helper method is available
  15. exp.get_from_options = function(rid, url, options, callback) {
  16. request.get({
  17. url: url,
  18. headers: {
  19. "User-Agent": "Crafatar (+https://crafatar.com)"
  20. },
  21. timeout: config.server.http_timeout,
  22. followRedirect: false,
  23. encoding: options.encoding || null,
  24. }, function(error, response, body) {
  25. // log url + code + description
  26. var code = response && response.statusCode;
  27. var logfunc = code && (code < 400 || code === 404) ? logging.debug : logging.warn;
  28. logfunc(rid, url, code || error && error.code, http_code[code]);
  29. // not necessarily used
  30. var e = new Error(code);
  31. e.name = "HTTP";
  32. e.code = "HTTPERROR";
  33. switch (code) {
  34. case 200:
  35. case 301:
  36. case 302: // never seen, but mojang might use it in future
  37. case 307: // never seen, but mojang might use it in future
  38. case 308: // never seen, but mojang might use it in future
  39. // these are okay
  40. break;
  41. case 204: // no content, used like 404 by mojang. making sure it really has no content
  42. case 404:
  43. // can be cached as null
  44. body = null;
  45. break;
  46. case 403: // Blocked by CloudFront :(
  47. case 429: // this shouldn't usually happen, but occasionally does
  48. case 500:
  49. case 502: // CloudFront can't reach mojang origin
  50. case 503:
  51. case 504:
  52. // we don't want to cache this
  53. error = error || e;
  54. body = null;
  55. break;
  56. default:
  57. if (!error) {
  58. // Probably 500 or the likes
  59. logging.error(rid, "Unexpected response:", code, body);
  60. }
  61. error = error || e;
  62. body = null;
  63. break;
  64. }
  65. if (body && !body.length) {
  66. // empty response
  67. body = null;
  68. }
  69. callback(body, response, error);
  70. });
  71. };
  72. // helper method for get_from_options, no options required
  73. exp.get_from = function(rid, url, callback) {
  74. exp.get_from_options(rid, url, {}, function(body, response, err) {
  75. callback(body, response, err);
  76. });
  77. };
  78. // gets the URL for a skin/cape from the profile
  79. // +type+ "SKIN" or "CAPE", specifies which to retrieve
  80. // callback: url, slim
  81. exp.get_uuid_info = function(profile, type, callback) {
  82. var properties = Object.get(profile, "properties") || [];
  83. properties.forEach(function(prop) {
  84. if (prop.name === "textures") {
  85. var json = new Buffer.from(prop.value, "base64").toString();
  86. profile = JSON.parse(json);
  87. }
  88. });
  89. var url = Object.get(profile, "textures." + type + ".url");
  90. var slim;
  91. if (type === "SKIN") {
  92. slim = Object.get(profile, "textures.SKIN.metadata.model") === "slim";
  93. }
  94. callback(null, url || null, !!slim);
  95. };
  96. // make a request to sessionserver for +uuid+
  97. // callback: error, profile
  98. exp.get_profile = function(rid, uuid, callback) {
  99. exp.get_from_options(rid, session_url + uuid, { encoding: "utf8" }, function(body, response, err) {
  100. try {
  101. body = body ? JSON.parse(body) : null;
  102. callback(err || null, body);
  103. } catch(e) {
  104. if (e instanceof SyntaxError) {
  105. logging.warn(rid, "Failed to parse JSON", e);
  106. logging.debug(rid, body);
  107. callback(err || null, null);
  108. } else {
  109. throw e;
  110. }
  111. }
  112. });
  113. };
  114. // get the skin URL and type for +userId+
  115. // +profile+ is used if +userId+ is a uuid
  116. // callback: error, url, slim
  117. exp.get_skin_info = function(rid, userId, profile, callback) {
  118. exp.get_uuid_info(profile, "SKIN", callback);
  119. };
  120. // get the cape URL for +userId+
  121. // +profile+ is used if +userId+ is a uuid
  122. exp.get_cape_url = function(rid, userId, profile, callback) {
  123. exp.get_uuid_info(profile, "CAPE", callback);
  124. };
  125. // download the +tex_hash+ image from the texture server
  126. // and save it in the +outpath+ file
  127. // callback: error, response, image buffer
  128. exp.save_texture = function(rid, tex_hash, outpath, callback) {
  129. if (tex_hash) {
  130. var textureurl = textures_url + tex_hash;
  131. exp.get_from(rid, textureurl, function(img, response, err) {
  132. if (err) {
  133. callback(err, response, null);
  134. } else {
  135. skins.save_image(img, outpath, function(img_err) {
  136. callback(img_err, response, img);
  137. });
  138. }
  139. });
  140. } else {
  141. callback(null, null, null);
  142. }
  143. };
  144. module.exports = exp;