httpExecutor.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.createHttpError = createHttpError;
  6. exports.parseJson = parseJson;
  7. exports.configureRequestOptionsFromUrl = configureRequestOptionsFromUrl;
  8. exports.safeGetHeader = safeGetHeader;
  9. exports.configureRequestOptions = configureRequestOptions;
  10. exports.safeStringifyJson = safeStringifyJson;
  11. exports.DigestTransform = exports.HttpExecutor = exports.HttpError = void 0;
  12. function _crypto() {
  13. const data = require("crypto");
  14. _crypto = function () {
  15. return data;
  16. };
  17. return data;
  18. }
  19. var _debug2 = _interopRequireDefault(require("debug"));
  20. function _fsExtraP() {
  21. const data = require("fs-extra-p");
  22. _fsExtraP = function () {
  23. return data;
  24. };
  25. return data;
  26. }
  27. function _stream() {
  28. const data = require("stream");
  29. _stream = function () {
  30. return data;
  31. };
  32. return data;
  33. }
  34. function _url() {
  35. const data = require("url");
  36. _url = function () {
  37. return data;
  38. };
  39. return data;
  40. }
  41. function _CancellationToken() {
  42. const data = require("./CancellationToken");
  43. _CancellationToken = function () {
  44. return data;
  45. };
  46. return data;
  47. }
  48. function _index() {
  49. const data = require("./index");
  50. _index = function () {
  51. return data;
  52. };
  53. return data;
  54. }
  55. function _ProgressCallbackTransform() {
  56. const data = require("./ProgressCallbackTransform");
  57. _ProgressCallbackTransform = function () {
  58. return data;
  59. };
  60. return data;
  61. }
  62. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  63. const debug = (0, _debug2.default)("electron-builder");
  64. function createHttpError(response, description = null) {
  65. return new HttpError(response.statusCode || -1, `${response.statusCode} ${response.statusMessage}` + (description == null ? "" : "\n" + JSON.stringify(description, null, " ")) + "\nHeaders: " + safeStringifyJson(response.headers), description);
  66. }
  67. const HTTP_STATUS_CODES = new Map([[429, "Too many requests"], [400, "Bad request"], [403, "Forbidden"], [404, "Not found"], [405, "Method not allowed"], [406, "Not acceptable"], [408, "Request timeout"], [413, "Request entity too large"], [500, "Internal server error"], [502, "Bad gateway"], [503, "Service unavailable"], [504, "Gateway timeout"], [505, "HTTP version not supported"]]);
  68. class HttpError extends Error {
  69. constructor(statusCode, message = `HTTP error: ${HTTP_STATUS_CODES.get(statusCode) || statusCode}`, description = null) {
  70. super(message);
  71. this.statusCode = statusCode;
  72. this.description = description;
  73. this.name = "HttpError";
  74. }
  75. }
  76. exports.HttpError = HttpError;
  77. function parseJson(result) {
  78. return result.then(it => it == null || it.length === 0 ? null : JSON.parse(it));
  79. }
  80. class HttpExecutor {
  81. constructor() {
  82. this.maxRedirects = 10;
  83. }
  84. request(options, cancellationToken = new (_CancellationToken().CancellationToken)(), data) {
  85. configureRequestOptions(options);
  86. const encodedData = data == null ? undefined : Buffer.from(JSON.stringify(data));
  87. if (encodedData != null) {
  88. options.method = "post";
  89. options.headers["Content-Type"] = "application/json";
  90. options.headers["Content-Length"] = encodedData.length;
  91. }
  92. return this.doApiRequest(options, cancellationToken, it => it.end(encodedData));
  93. }
  94. doApiRequest(options, cancellationToken, requestProcessor, redirectCount = 0) {
  95. if (debug.enabled) {
  96. debug(`Request: ${safeStringifyJson(options)}`);
  97. }
  98. return cancellationToken.createPromise((resolve, reject, onCancel) => {
  99. const request = this.doRequest(options, response => {
  100. try {
  101. this.handleResponse(response, options, cancellationToken, resolve, reject, redirectCount, requestProcessor);
  102. } catch (e) {
  103. reject(e);
  104. }
  105. });
  106. this.addErrorAndTimeoutHandlers(request, reject);
  107. this.addRedirectHandlers(request, options, reject, redirectCount, options => {
  108. this.doApiRequest(options, cancellationToken, requestProcessor, redirectCount).then(resolve).catch(reject);
  109. });
  110. requestProcessor(request, reject);
  111. onCancel(() => request.abort());
  112. });
  113. } // noinspection JSUnusedLocalSymbols
  114. addRedirectHandlers(request, options, reject, redirectCount, handler) {// not required for NodeJS
  115. }
  116. addErrorAndTimeoutHandlers(request, reject) {
  117. this.addTimeOutHandler(request, reject);
  118. request.on("error", reject);
  119. request.on("aborted", () => {
  120. reject(new Error("Request has been aborted by the server"));
  121. });
  122. }
  123. handleResponse(response, options, cancellationToken, resolve, reject, redirectCount, requestProcessor) {
  124. if (debug.enabled) {
  125. debug(`Response: ${response.statusCode} ${response.statusMessage}, request options: ${safeStringifyJson(options)}`);
  126. } // we handle any other >= 400 error on request end (read detailed message in the response body)
  127. if (response.statusCode === 404) {
  128. // error is clear, we don't need to read detailed error description
  129. reject(createHttpError(response, `method: ${options.method} url: ${options.protocol || "https:"}//${options.hostname}${options.path}
  130. Please double check that your authentication token is correct. Due to security reasons actual status maybe not reported, but 404.
  131. `));
  132. return;
  133. } else if (response.statusCode === 204) {
  134. // on DELETE request
  135. resolve();
  136. return;
  137. }
  138. const redirectUrl = safeGetHeader(response, "location");
  139. if (redirectUrl != null) {
  140. if (redirectCount > 10) {
  141. reject(new Error("Too many redirects (> 10)"));
  142. return;
  143. }
  144. this.doApiRequest(HttpExecutor.prepareRedirectUrlOptions(redirectUrl, options), cancellationToken, requestProcessor, redirectCount).then(resolve).catch(reject);
  145. return;
  146. }
  147. let data = "";
  148. response.setEncoding("utf8");
  149. response.on("data", chunk => data += chunk);
  150. response.on("end", () => {
  151. try {
  152. if (response.statusCode != null && response.statusCode >= 400) {
  153. const contentType = safeGetHeader(response, "content-type");
  154. const isJson = contentType != null && (Array.isArray(contentType) ? contentType.find(it => it.includes("json")) != null : contentType.includes("json"));
  155. reject(createHttpError(response, isJson ? JSON.parse(data) : data));
  156. } else {
  157. resolve(data.length === 0 ? null : data);
  158. }
  159. } catch (e) {
  160. reject(e);
  161. }
  162. });
  163. }
  164. doDownload(requestOptions, destination, redirectCount, options, callback, onCancel) {
  165. const request = this.doRequest(requestOptions, response => {
  166. if (response.statusCode >= 400) {
  167. callback(new Error(`Cannot download "${requestOptions.protocol || "https:"}//${requestOptions.hostname}${requestOptions.path}", status ${response.statusCode}: ${response.statusMessage}`));
  168. return;
  169. }
  170. const redirectUrl = safeGetHeader(response, "location");
  171. if (redirectUrl != null) {
  172. if (redirectCount < this.maxRedirects) {
  173. this.doDownload(HttpExecutor.prepareRedirectUrlOptions(redirectUrl, requestOptions), destination, redirectCount++, options, callback, onCancel);
  174. } else {
  175. callback(new Error(`Too many redirects (> ${this.maxRedirects})`));
  176. }
  177. return;
  178. }
  179. configurePipes(options, response, destination, callback, options.cancellationToken);
  180. });
  181. this.addErrorAndTimeoutHandlers(request, callback);
  182. this.addRedirectHandlers(request, requestOptions, callback, redirectCount, requestOptions => {
  183. this.doDownload(requestOptions, destination, redirectCount++, options, callback, onCancel);
  184. });
  185. onCancel(() => request.abort());
  186. request.end();
  187. }
  188. addTimeOutHandler(request, callback) {
  189. request.on("socket", socket => {
  190. socket.setTimeout(60 * 1000, () => {
  191. callback(new Error("Request timed out"));
  192. request.abort();
  193. });
  194. });
  195. }
  196. static prepareRedirectUrlOptions(redirectUrl, options) {
  197. const newOptions = configureRequestOptionsFromUrl(redirectUrl, Object.assign({}, options));
  198. if (newOptions.headers != null && newOptions.headers.Authorization != null && newOptions.headers.Authorization.startsWith("token")) {
  199. const parsedNewUrl = new (_url().URL)(redirectUrl);
  200. if (parsedNewUrl.hostname.endsWith(".amazonaws.com")) {
  201. delete newOptions.headers.Authorization;
  202. }
  203. }
  204. return newOptions;
  205. }
  206. }
  207. exports.HttpExecutor = HttpExecutor;
  208. function configureRequestOptionsFromUrl(url, options) {
  209. const parsedUrl = (0, _url().parse)(url);
  210. options.protocol = parsedUrl.protocol;
  211. options.hostname = parsedUrl.hostname;
  212. if (parsedUrl.port == null) {
  213. if (options.port != null) {
  214. delete options.port;
  215. }
  216. } else {
  217. options.port = parsedUrl.port;
  218. }
  219. options.path = parsedUrl.path;
  220. return configureRequestOptions(options);
  221. }
  222. class DigestTransform extends _stream().Transform {
  223. constructor(expected, algorithm = "sha512", encoding = "base64") {
  224. super();
  225. this.expected = expected;
  226. this.algorithm = algorithm;
  227. this.encoding = encoding;
  228. this._actual = null;
  229. this.isValidateOnEnd = true;
  230. this.digester = (0, _crypto().createHash)(algorithm);
  231. } // noinspection JSUnusedGlobalSymbols
  232. get actual() {
  233. return this._actual;
  234. } // noinspection JSUnusedGlobalSymbols
  235. _transform(chunk, encoding, callback) {
  236. this.digester.update(chunk);
  237. callback(null, chunk);
  238. } // noinspection JSUnusedGlobalSymbols
  239. _flush(callback) {
  240. this._actual = this.digester.digest(this.encoding);
  241. if (this.isValidateOnEnd) {
  242. try {
  243. this.validate();
  244. } catch (e) {
  245. callback(e);
  246. return;
  247. }
  248. }
  249. callback(null);
  250. }
  251. validate() {
  252. if (this._actual == null) {
  253. throw (0, _index().newError)("Not finished yet", "ERR_STREAM_NOT_FINISHED");
  254. }
  255. if (this._actual !== this.expected) {
  256. throw (0, _index().newError)(`${this.algorithm} checksum mismatch, expected ${this.expected}, got ${this._actual}`, "ERR_CHECKSUM_MISMATCH");
  257. }
  258. return null;
  259. }
  260. }
  261. exports.DigestTransform = DigestTransform;
  262. function checkSha2(sha2Header, sha2, callback) {
  263. if (sha2Header != null && sha2 != null) {
  264. // todo why bintray doesn't send this header always
  265. if (sha2Header == null) {
  266. callback(new Error("checksum is required, but server response doesn't contain X-Checksum-Sha2 header"));
  267. return false;
  268. } else if (sha2Header !== sha2) {
  269. callback(new Error(`checksum mismatch: expected ${sha2} but got ${sha2Header} (X-Checksum-Sha2 header)`));
  270. return false;
  271. }
  272. }
  273. return true;
  274. }
  275. function safeGetHeader(response, headerKey) {
  276. const value = response.headers[headerKey];
  277. if (value == null) {
  278. return null;
  279. } else if (Array.isArray(value)) {
  280. // electron API
  281. return value.length === 0 ? null : value[value.length - 1];
  282. } else {
  283. return value;
  284. }
  285. }
  286. function configurePipes(options, response, destination, callback, cancellationToken) {
  287. if (!checkSha2(safeGetHeader(response, "X-Checksum-Sha2"), options.sha2, callback)) {
  288. return;
  289. }
  290. const streams = [];
  291. if (options.onProgress != null) {
  292. const contentLength = safeGetHeader(response, "content-length");
  293. if (contentLength != null) {
  294. streams.push(new (_ProgressCallbackTransform().ProgressCallbackTransform)(parseInt(contentLength, 10), options.cancellationToken, options.onProgress));
  295. }
  296. }
  297. const sha512 = options.sha512;
  298. if (sha512 != null) {
  299. streams.push(new DigestTransform(sha512, "sha512", sha512.length === 128 && !sha512.includes("+") && !sha512.includes("Z") && !sha512.includes("=") ? "hex" : "base64"));
  300. } else if (options.sha2 != null) {
  301. streams.push(new DigestTransform(options.sha2, "sha256", "hex"));
  302. }
  303. const fileOut = (0, _fsExtraP().createWriteStream)(destination);
  304. streams.push(fileOut);
  305. let lastStream = response;
  306. for (const stream of streams) {
  307. stream.on("error", error => {
  308. if (!cancellationToken.cancelled) {
  309. callback(error);
  310. }
  311. });
  312. lastStream = lastStream.pipe(stream);
  313. }
  314. fileOut.on("finish", () => {
  315. fileOut.close(callback);
  316. });
  317. }
  318. function configureRequestOptions(options, token, method) {
  319. if (method != null) {
  320. options.method = method;
  321. }
  322. let headers = options.headers;
  323. if (headers == null) {
  324. headers = {};
  325. options.headers = headers;
  326. }
  327. if (token != null) {
  328. headers.authorization = token.startsWith("Basic") ? token : `token ${token}`;
  329. }
  330. if (headers["User-Agent"] == null) {
  331. headers["User-Agent"] = "electron-builder";
  332. }
  333. if (method == null || method === "GET" || headers["Cache-Control"] == null) {
  334. headers["Cache-Control"] = "no-cache";
  335. } // do not specify for node (in any case we use https module)
  336. if (options.protocol == null && process.versions.electron != null) {
  337. options.protocol = "https:";
  338. }
  339. return options;
  340. }
  341. function safeStringifyJson(data, skippedNames) {
  342. return JSON.stringify(data, (name, value) => {
  343. if (name.endsWith("authorization") || name.endsWith("Password") || name.endsWith("PASSWORD") || name.endsWith("Token") || name.includes("password") || name.includes("token") || skippedNames != null && skippedNames.has(name)) {
  344. return "<stripped sensitive data>";
  345. }
  346. return value;
  347. }, 2);
  348. }
  349. //# sourceMappingURL=httpExecutor.js.map