http.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. "use strict";
  2. const http = require("http");
  3. const https = require("https");
  4. const { ono } = require("@jsdevtools/ono");
  5. const url = require("../util/url");
  6. const { ResolverError } = require("../util/errors");
  7. module.exports = {
  8. /**
  9. * The order that this resolver will run, in relation to other resolvers.
  10. *
  11. * @type {number}
  12. */
  13. order: 200,
  14. /**
  15. * HTTP headers to send when downloading files.
  16. *
  17. * @example:
  18. * {
  19. * "User-Agent": "JSON Schema $Ref Parser",
  20. * Accept: "application/json"
  21. * }
  22. *
  23. * @type {object}
  24. */
  25. headers: null,
  26. /**
  27. * HTTP request timeout (in milliseconds).
  28. *
  29. * @type {number}
  30. */
  31. timeout: 5000, // 5 seconds
  32. /**
  33. * The maximum number of HTTP redirects to follow.
  34. * To disable automatic following of redirects, set this to zero.
  35. *
  36. * @type {number}
  37. */
  38. redirects: 5,
  39. /**
  40. * The `withCredentials` option of XMLHttpRequest.
  41. * Set this to `true` if you're downloading files from a CORS-enabled server that requires authentication
  42. *
  43. * @type {boolean}
  44. */
  45. withCredentials: false,
  46. /**
  47. * Determines whether this resolver can read a given file reference.
  48. * Resolvers that return true will be tried in order, until one successfully resolves the file.
  49. * Resolvers that return false will not be given a chance to resolve the file.
  50. *
  51. * @param {object} file - An object containing information about the referenced file
  52. * @param {string} file.url - The full URL of the referenced file
  53. * @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
  54. * @returns {boolean}
  55. */
  56. canRead (file) {
  57. return url.isHttp(file.url);
  58. },
  59. /**
  60. * Reads the given URL and returns its raw contents as a Buffer.
  61. *
  62. * @param {object} file - An object containing information about the referenced file
  63. * @param {string} file.url - The full URL of the referenced file
  64. * @param {string} file.extension - The lowercased file extension (e.g. ".txt", ".html", etc.)
  65. * @returns {Promise<Buffer>}
  66. */
  67. read (file) {
  68. let u = url.parse(file.url);
  69. if (process.browser && !u.protocol) {
  70. // Use the protocol of the current page
  71. u.protocol = url.parse(location.href).protocol;
  72. }
  73. return download(u, this);
  74. }
  75. };
  76. /**
  77. * Downloads the given file.
  78. *
  79. * @param {Url|string} u - The url to download (can be a parsed {@link Url} object)
  80. * @param {object} httpOptions - The `options.resolve.http` object
  81. * @param {number} [redirects] - The redirect URLs that have already been followed
  82. *
  83. * @returns {Promise<Buffer>}
  84. * The promise resolves with the raw downloaded data, or rejects if there is an HTTP error.
  85. */
  86. function download (u, httpOptions, redirects) {
  87. return new Promise(((resolve, reject) => {
  88. u = url.parse(u);
  89. redirects = redirects || [];
  90. redirects.push(u.href);
  91. get(u, httpOptions)
  92. .then((res) => {
  93. if (res.statusCode >= 400) {
  94. throw ono({ status: res.statusCode }, `HTTP ERROR ${res.statusCode}`);
  95. }
  96. else if (res.statusCode >= 300) {
  97. if (redirects.length > httpOptions.redirects) {
  98. reject(new ResolverError(ono({ status: res.statusCode },
  99. `Error downloading ${redirects[0]}. \nToo many redirects: \n ${redirects.join(" \n ")}`)));
  100. }
  101. else if (!res.headers.location) {
  102. throw ono({ status: res.statusCode }, `HTTP ${res.statusCode} redirect with no location header`);
  103. }
  104. else {
  105. // console.log('HTTP %d redirect %s -> %s', res.statusCode, u.href, res.headers.location);
  106. let redirectTo = url.resolve(u, res.headers.location);
  107. download(redirectTo, httpOptions, redirects).then(resolve, reject);
  108. }
  109. }
  110. else {
  111. resolve(res.body || Buffer.alloc(0));
  112. }
  113. })
  114. .catch((err) => {
  115. reject(new ResolverError(ono(err, `Error downloading ${u.href}`), u.href));
  116. });
  117. }));
  118. }
  119. /**
  120. * Sends an HTTP GET request.
  121. *
  122. * @param {Url} u - A parsed {@link Url} object
  123. * @param {object} httpOptions - The `options.resolve.http` object
  124. *
  125. * @returns {Promise<Response>}
  126. * The promise resolves with the HTTP Response object.
  127. */
  128. function get (u, httpOptions) {
  129. return new Promise(((resolve, reject) => {
  130. // console.log('GET', u.href);
  131. let protocol = u.protocol === "https:" ? https : http;
  132. let req = protocol.get({
  133. hostname: u.hostname,
  134. port: u.port,
  135. path: u.path,
  136. auth: u.auth,
  137. protocol: u.protocol,
  138. headers: httpOptions.headers || {},
  139. withCredentials: httpOptions.withCredentials
  140. });
  141. if (typeof req.setTimeout === "function") {
  142. req.setTimeout(httpOptions.timeout);
  143. }
  144. req.on("timeout", () => {
  145. req.abort();
  146. });
  147. req.on("error", reject);
  148. req.once("response", (res) => {
  149. res.body = Buffer.alloc(0);
  150. res.on("data", (data) => {
  151. res.body = Buffer.concat([res.body, Buffer.from(data)]);
  152. });
  153. res.on("error", reject);
  154. res.on("end", () => {
  155. resolve(res);
  156. });
  157. });
  158. }));
  159. }