networking.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. var logging = require("./logging");
  2. var request = require("request");
  3. var config = require("../config");
  4. var skins = require("./skins");
  5. var http = require("http");
  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. // count requests made to session_url in the last 1000ms
  10. var session_requests = [];
  11. var exp = {};
  12. // returns the amount of outgoing session requests made in the last 1000ms
  13. function req_count() {
  14. var index = session_requests.findIndex((i) => i >= Date.now() - 1000);
  15. if (index >= 0) {
  16. return session_requests.length - index;
  17. } else {
  18. return 0;
  19. }
  20. }
  21. // deletes all entries in session_requests, should be called every 1000ms
  22. exp.resetCounter = function() {
  23. var count = req_count();
  24. if (count) {
  25. var logfunc = count >= config.server.sessions_rate_limit ? logging.warn : logging.debug;
  26. logfunc('Clearing old session requests (count was ' + count + ')');
  27. session_requests.splice(0, session_requests.length - count);
  28. } else {
  29. session_requests = []
  30. }
  31. }
  32. // performs a GET request to the +url+
  33. // +options+ object includes these options:
  34. // encoding (string), default is to return a buffer
  35. // callback: the body, response,
  36. // and error buffer. get_from helper method is available
  37. exp.get_from_options = function(rid, url, options, callback) {
  38. var is_session_req = config.server.sessions_rate_limit && url.startsWith(session_url);
  39. // This is to prevent being blocked by CloudFront for exceeding the rate limit
  40. if (is_session_req && req_count() >= config.server.sessions_rate_limit) {
  41. var e = new Error("Skipped, rate limit exceeded");
  42. e.name = "HTTP";
  43. e.code = "RATELIMIT";
  44. var response = new http.IncomingMessage();
  45. response.statusCode = 403;
  46. callback(null, response, e);
  47. } else {
  48. is_session_req && session_requests.push(Date.now());
  49. request.get({
  50. url: url,
  51. headers: {
  52. "User-Agent": "Crafatar (+https://crafatar.com)"
  53. },
  54. timeout: config.server.http_timeout,
  55. followRedirect: false,
  56. encoding: options.encoding || null,
  57. }, function(error, response, body) {
  58. // log url + code + description
  59. var code = response && response.statusCode;
  60. var logfunc = code && (code < 400 || code === 404) ? logging.debug : logging.warn;
  61. logfunc(rid, url, code || error && error.code, http.STATUS_CODES[code]);
  62. // not necessarily used
  63. var e = new Error(code);
  64. e.name = "HTTP";
  65. e.code = "HTTPERROR";
  66. switch (code) {
  67. case 200:
  68. case 301:
  69. case 302: // never seen, but mojang might use it in future
  70. case 307: // never seen, but mojang might use it in future
  71. case 308: // never seen, but mojang might use it in future
  72. // these are okay
  73. break;
  74. case 204: // no content, used like 404 by mojang. making sure it really has no content
  75. case 404:
  76. // can be cached as null
  77. body = null;
  78. break;
  79. case 403: // Blocked by CloudFront :(
  80. case 429: // this shouldn't usually happen, but occasionally does
  81. case 500:
  82. case 502: // CloudFront can't reach mojang origin
  83. case 503:
  84. case 504:
  85. // we don't want to cache this
  86. error = error || e;
  87. body = null;
  88. break;
  89. default:
  90. if (!error) {
  91. // Probably 500 or the likes
  92. logging.error(rid, "Unexpected response:", code, body);
  93. }
  94. error = error || e;
  95. body = null;
  96. break;
  97. }
  98. if (body && !body.length) {
  99. // empty response
  100. body = null;
  101. }
  102. callback(body, response, error);
  103. });
  104. }
  105. };
  106. // helper method for get_from_options, no options required
  107. exp.get_from = function(rid, url, callback) {
  108. exp.get_from_options(rid, url, {}, function(body, response, err) {
  109. callback(body, response, err);
  110. });
  111. };
  112. // gets the URL for a skin/cape from the profile
  113. // +type+ "SKIN" or "CAPE", specifies which to retrieve
  114. // callback: url, slim
  115. exp.get_uuid_info = function(profile, type, callback) {
  116. var properties = Object.get(profile, "properties") || [];
  117. properties.forEach(function(prop) {
  118. if (prop.name === "textures") {
  119. var json = new Buffer.from(prop.value, "base64").toString();
  120. profile = JSON.parse(json);
  121. }
  122. });
  123. var url = Object.get(profile, "textures." + type + ".url");
  124. var slim;
  125. if (type === "SKIN") {
  126. slim = Object.get(profile, "textures.SKIN.metadata.model") === "slim";
  127. }
  128. callback(null, url || null, !!slim);
  129. };
  130. // make a request to sessionserver for +uuid+
  131. // callback: error, profile
  132. exp.get_profile = function(rid, uuid, callback) {
  133. exp.get_from_options(rid, session_url + uuid, { encoding: "utf8" }, function(body, response, err) {
  134. try {
  135. body = body ? JSON.parse(body) : null;
  136. callback(err || null, body);
  137. } catch(e) {
  138. if (e instanceof SyntaxError) {
  139. logging.warn(rid, "Failed to parse JSON", e);
  140. logging.debug(rid, body);
  141. callback(err || null, null);
  142. } else {
  143. throw e;
  144. }
  145. }
  146. });
  147. };
  148. // get the skin URL and type for +userId+
  149. // +profile+ is used if +userId+ is a uuid
  150. // callback: error, url, slim
  151. exp.get_skin_info = function(rid, userId, profile, callback) {
  152. exp.get_uuid_info(profile, "SKIN", callback);
  153. };
  154. // get the cape URL for +userId+
  155. // +profile+ is used if +userId+ is a uuid
  156. exp.get_cape_url = function(rid, userId, profile, callback) {
  157. exp.get_uuid_info(profile, "CAPE", callback);
  158. };
  159. // download the +tex_hash+ image from the texture server
  160. // and save it in the +outpath+ file
  161. // callback: error, response, image buffer
  162. exp.save_texture = function(rid, tex_hash, outpath, callback) {
  163. if (tex_hash) {
  164. var textureurl = textures_url + tex_hash;
  165. exp.get_from(rid, textureurl, function(img, response, err) {
  166. if (err) {
  167. callback(err, response, null);
  168. } else {
  169. skins.save_image(img, outpath, function(img_err) {
  170. callback(img_err, response, img);
  171. });
  172. }
  173. });
  174. } else {
  175. callback(null, null, null);
  176. }
  177. };
  178. module.exports = exp;