httpExecutor.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.HttpExecutor = exports.HttpError = exports.executorHolder = exports.HttpExecutorHolder = undefined;
  6. exports.download = download;
  7. exports.request = request;
  8. exports.configureRequestOptions = configureRequestOptions;
  9. exports.dumpRequestOptions = dumpRequestOptions;
  10. var _crypto;
  11. function _load_crypto() {
  12. return _crypto = require("crypto");
  13. }
  14. var _debug2;
  15. function _load_debug() {
  16. return _debug2 = _interopRequireDefault(require("debug"));
  17. }
  18. var _fsExtraP;
  19. function _load_fsExtraP() {
  20. return _fsExtraP = require("fs-extra-p");
  21. }
  22. var _jsYaml;
  23. function _load_jsYaml() {
  24. return _jsYaml = require("js-yaml");
  25. }
  26. var _stream;
  27. function _load_stream() {
  28. return _stream = require("stream");
  29. }
  30. var _url;
  31. function _load_url() {
  32. return _url = require("url");
  33. }
  34. var _CancellationToken;
  35. function _load_CancellationToken() {
  36. return _CancellationToken = require("./CancellationToken");
  37. }
  38. var _ProgressCallbackTransform;
  39. function _load_ProgressCallbackTransform() {
  40. return _ProgressCallbackTransform = require("./ProgressCallbackTransform");
  41. }
  42. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  43. const debug = (0, (_debug2 || _load_debug()).default)("electron-builder");
  44. class HttpExecutorHolder {
  45. get httpExecutor() {
  46. if (this._httpExecutor == null) {
  47. this._httpExecutor = new (require("electron-builder-util/out/nodeHttpExecutor").NodeHttpExecutor)();
  48. }
  49. return this._httpExecutor;
  50. }
  51. set httpExecutor(value) {
  52. this._httpExecutor = value;
  53. }
  54. }
  55. exports.HttpExecutorHolder = HttpExecutorHolder;
  56. const executorHolder = exports.executorHolder = new HttpExecutorHolder();
  57. function download(url, destination, options) {
  58. return executorHolder.httpExecutor.download(url, destination, options || { cancellationToken: new (_CancellationToken || _load_CancellationToken()).CancellationToken() });
  59. }
  60. class HttpError extends Error {
  61. constructor(response) {
  62. let description = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
  63. super(response.statusCode + " " + response.statusMessage + (description == null ? "" : "\n" + JSON.stringify(description, null, " ")) + "\nHeaders: " + JSON.stringify(response.headers, null, " "));
  64. this.response = response;
  65. this.description = description;
  66. this.name = "HttpError";
  67. }
  68. }
  69. exports.HttpError = HttpError;
  70. class HttpExecutor {
  71. constructor() {
  72. this.maxRedirects = 10;
  73. }
  74. request(options, cancellationToken, data) {
  75. configureRequestOptions(options);
  76. const encodedData = data == null ? undefined : new Buffer(JSON.stringify(data));
  77. if (encodedData != null) {
  78. options.method = "post";
  79. options.headers["Content-Type"] = "application/json";
  80. options.headers["Content-Length"] = encodedData.length;
  81. }
  82. return this.doApiRequest(options, cancellationToken, it => it.end(encodedData), 0);
  83. }
  84. handleResponse(response, options, cancellationToken, resolve, reject, redirectCount, requestProcessor) {
  85. if (debug.enabled) {
  86. debug(`Response status: ${response.statusCode} ${response.statusMessage}, request options: ${dumpRequestOptions(options)}`);
  87. }
  88. // we handle any other >= 400 error on request end (read detailed message in the response body)
  89. if (response.statusCode === 404) {
  90. // error is clear, we don't need to read detailed error description
  91. reject(new HttpError(response, `method: ${options.method} url: https://${options.hostname}${options.path}
  92. Please double check that your authentication token is correct. Due to security reasons actual status maybe not reported, but 404.
  93. `));
  94. return;
  95. } else if (response.statusCode === 204) {
  96. // on DELETE request
  97. resolve();
  98. return;
  99. }
  100. const redirectUrl = safeGetHeader(response, "location");
  101. if (redirectUrl != null) {
  102. if (redirectCount > 10) {
  103. reject(new Error("Too many redirects (> 10)"));
  104. return;
  105. }
  106. const newUrl = (0, (_url || _load_url()).parse)(redirectUrl);
  107. this.doApiRequest(Object.assign({}, options, newUrl), cancellationToken, requestProcessor, redirectCount).then(resolve).catch(reject);
  108. return;
  109. }
  110. let data = "";
  111. response.setEncoding("utf8");
  112. response.on("data", chunk => {
  113. data += chunk;
  114. });
  115. response.on("end", () => {
  116. try {
  117. const contentType = response.headers["content-type"];
  118. const isJson = contentType != null && (Array.isArray(contentType) ? contentType.find(it => it.indexOf("json") !== -1) != null : contentType.indexOf("json") !== -1);
  119. if (response.statusCode != null && response.statusCode >= 400) {
  120. reject(new HttpError(response, isJson ? JSON.parse(data) : data));
  121. } else {
  122. const pathname = options.pathname || options.path;
  123. if (data.length === 0) {
  124. resolve();
  125. } else if (pathname != null && pathname.endsWith(".yml")) {
  126. resolve((0, (_jsYaml || _load_jsYaml()).safeLoad)(data));
  127. } else {
  128. resolve(isJson || pathname != null && pathname.endsWith(".json") ? JSON.parse(data) : data);
  129. }
  130. }
  131. } catch (e) {
  132. reject(e);
  133. }
  134. });
  135. }
  136. doDownload(requestOptions, destination, redirectCount, options, callback, onCancel) {
  137. const request = this.doRequest(requestOptions, response => {
  138. if (response.statusCode >= 400) {
  139. callback(new Error(`Cannot download "${requestOptions.protocol || "https"}://${requestOptions.hostname}/${requestOptions.path}", status ${response.statusCode}: ${response.statusMessage}`));
  140. return;
  141. }
  142. const redirectUrl = safeGetHeader(response, "location");
  143. if (redirectUrl != null) {
  144. if (redirectCount < this.maxRedirects) {
  145. const parsedUrl = (0, (_url || _load_url()).parse)(redirectUrl);
  146. this.doDownload(Object.assign({}, requestOptions, {
  147. hostname: parsedUrl.hostname,
  148. path: parsedUrl.path,
  149. port: parsedUrl.port == null ? undefined : parsedUrl.port
  150. }), destination, redirectCount++, options, callback, onCancel);
  151. } else {
  152. callback(new Error(`Too many redirects (> ${this.maxRedirects})`));
  153. }
  154. return;
  155. }
  156. configurePipes(options, response, destination, callback, options.cancellationToken);
  157. });
  158. this.addTimeOutHandler(request, callback);
  159. request.on("error", callback);
  160. onCancel(() => request.abort());
  161. request.end();
  162. }
  163. addTimeOutHandler(request, callback) {
  164. request.on("socket", function (socket) {
  165. socket.setTimeout(60 * 1000, () => {
  166. callback(new Error("Request timed out"));
  167. request.abort();
  168. });
  169. });
  170. }
  171. }
  172. exports.HttpExecutor = HttpExecutor;
  173. class DigestTransform extends (_stream || _load_stream()).Transform {
  174. constructor(expected) {
  175. super();
  176. this.expected = expected;
  177. this.digester = (0, (_crypto || _load_crypto()).createHash)("sha256");
  178. }
  179. _transform(chunk, encoding, callback) {
  180. this.digester.update(chunk);
  181. callback(null, chunk);
  182. }
  183. _flush(callback) {
  184. const hash = this.digester.digest("hex");
  185. callback(hash === this.expected ? null : new Error(`SHA2 checksum mismatch, expected ${this.expected}, got ${hash}`));
  186. }
  187. }
  188. function request(options, cancellationToken, data) {
  189. return executorHolder.httpExecutor.request(options, cancellationToken, data);
  190. }
  191. function checkSha2(sha2Header, sha2, callback) {
  192. if (sha2Header != null && sha2 != null) {
  193. // todo why bintray doesn't send this header always
  194. if (sha2Header == null) {
  195. callback(new Error("checksum is required, but server response doesn't contain X-Checksum-Sha2 header"));
  196. return false;
  197. } else if (sha2Header !== sha2) {
  198. callback(new Error(`checksum mismatch: expected ${sha2} but got ${sha2Header} (X-Checksum-Sha2 header)`));
  199. return false;
  200. }
  201. }
  202. return true;
  203. }
  204. function safeGetHeader(response, headerKey) {
  205. const value = response.headers[headerKey];
  206. if (value == null) {
  207. return null;
  208. } else if (Array.isArray(value)) {
  209. // electron API
  210. return value.length === 0 ? null : value[value.length - 1];
  211. } else {
  212. return value;
  213. }
  214. }
  215. function configurePipes(options, response, destination, callback, cancellationToken) {
  216. if (!checkSha2(safeGetHeader(response, "X-Checksum-Sha2"), options.sha2, callback)) {
  217. return;
  218. }
  219. const streams = [];
  220. if (options.onProgress != null) {
  221. const contentLength = safeGetHeader(response, "content-length");
  222. if (contentLength != null) {
  223. streams.push(new (_ProgressCallbackTransform || _load_ProgressCallbackTransform()).ProgressCallbackTransform(parseInt(contentLength, 10), options.cancellationToken, options.onProgress));
  224. }
  225. }
  226. if (options.sha2 != null) {
  227. streams.push(new DigestTransform(options.sha2));
  228. }
  229. const fileOut = (0, (_fsExtraP || _load_fsExtraP()).createWriteStream)(destination);
  230. streams.push(fileOut);
  231. let lastStream = response;
  232. for (const stream of streams) {
  233. stream.on("error", error => {
  234. if (!cancellationToken.cancelled) {
  235. callback(error);
  236. }
  237. });
  238. lastStream = lastStream.pipe(stream);
  239. }
  240. fileOut.on("finish", () => {
  241. fileOut.close(callback);
  242. });
  243. }
  244. function configureRequestOptions(options, token, method) {
  245. if (method != null) {
  246. options.method = method;
  247. }
  248. let headers = options.headers;
  249. if (headers == null) {
  250. headers = {};
  251. options.headers = headers;
  252. }
  253. if (token != null) {
  254. headers.authorization = token.startsWith("Basic") ? token : `token ${token}`;
  255. }
  256. if (headers["User-Agent"] == null) {
  257. headers["User-Agent"] = "electron-builder";
  258. }
  259. if (method == null || method === "GET" || headers["Cache-Control"] == null) {
  260. headers["Cache-Control"] = "no-cache";
  261. }
  262. if (options.protocol == null) {
  263. options.protocol = "https:";
  264. }
  265. return options;
  266. }
  267. function dumpRequestOptions(options) {
  268. const safe = Object.assign({}, options);
  269. if (safe.headers != null && safe.headers.authorization != null) {
  270. safe.headers.authorization = "<skipped>";
  271. }
  272. return JSON.stringify(safe, null, 2);
  273. }
  274. //# sourceMappingURL=httpExecutor.js.map