api-request.js 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764
  1. /*! firebase-admin v12.1.1 */
  2. "use strict";
  3. /*!
  4. * @license
  5. * Copyright 2017 Google Inc.
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. */
  19. Object.defineProperty(exports, "__esModule", { value: true });
  20. exports.ExponentialBackoffPoller = exports.ApiSettings = exports.AuthorizedHttpClient = exports.parseHttpResponse = exports.HttpClient = exports.defaultRetryConfig = exports.HttpError = void 0;
  21. const error_1 = require("./error");
  22. const validator = require("./validator");
  23. const http = require("http");
  24. const https = require("https");
  25. const url = require("url");
  26. const events_1 = require("events");
  27. class DefaultHttpResponse {
  28. /**
  29. * Constructs a new HttpResponse from the given LowLevelResponse.
  30. */
  31. constructor(resp) {
  32. this.status = resp.status;
  33. this.headers = resp.headers;
  34. this.text = resp.data;
  35. try {
  36. if (!resp.data) {
  37. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'HTTP response missing data.');
  38. }
  39. this.parsedData = JSON.parse(resp.data);
  40. }
  41. catch (err) {
  42. this.parsedData = undefined;
  43. this.parseError = err;
  44. }
  45. this.request = `${resp.config.method} ${resp.config.url}`;
  46. }
  47. get data() {
  48. if (this.isJson()) {
  49. return this.parsedData;
  50. }
  51. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, `Error while parsing response data: "${this.parseError.toString()}". Raw server ` +
  52. `response: "${this.text}". Status code: "${this.status}". Outgoing ` +
  53. `request: "${this.request}."`);
  54. }
  55. isJson() {
  56. return typeof this.parsedData !== 'undefined';
  57. }
  58. }
  59. /**
  60. * Represents a multipart HTTP response. Parts that constitute the response body can be accessed
  61. * via the multipart getter. Getters for text and data throw errors.
  62. */
  63. class MultipartHttpResponse {
  64. constructor(resp) {
  65. this.status = resp.status;
  66. this.headers = resp.headers;
  67. this.multipart = resp.multipart;
  68. }
  69. get text() {
  70. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, 'Unable to parse multipart payload as text');
  71. }
  72. get data() {
  73. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.UNABLE_TO_PARSE_RESPONSE, 'Unable to parse multipart payload as JSON');
  74. }
  75. isJson() {
  76. return false;
  77. }
  78. }
  79. class HttpError extends Error {
  80. constructor(response) {
  81. super(`Server responded with status ${response.status}.`);
  82. this.response = response;
  83. // Set the prototype so that instanceof checks will work correctly.
  84. // See: https://github.com/Microsoft/TypeScript/issues/13965
  85. Object.setPrototypeOf(this, HttpError.prototype);
  86. }
  87. }
  88. exports.HttpError = HttpError;
  89. /**
  90. * Default retry configuration for HTTP requests. Retries up to 4 times on connection reset and timeout errors
  91. * as well as HTTP 503 errors. Exposed as a function to ensure that every HttpClient gets its own RetryConfig
  92. * instance.
  93. */
  94. function defaultRetryConfig() {
  95. return {
  96. maxRetries: 4,
  97. statusCodes: [503],
  98. ioErrorCodes: ['ECONNRESET', 'ETIMEDOUT'],
  99. backOffFactor: 0.5,
  100. maxDelayInMillis: 60 * 1000,
  101. };
  102. }
  103. exports.defaultRetryConfig = defaultRetryConfig;
  104. /**
  105. * Ensures that the given RetryConfig object is valid.
  106. *
  107. * @param retry - The configuration to be validated.
  108. */
  109. function validateRetryConfig(retry) {
  110. if (!validator.isNumber(retry.maxRetries) || retry.maxRetries < 0) {
  111. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_ARGUMENT, 'maxRetries must be a non-negative integer');
  112. }
  113. if (typeof retry.backOffFactor !== 'undefined') {
  114. if (!validator.isNumber(retry.backOffFactor) || retry.backOffFactor < 0) {
  115. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_ARGUMENT, 'backOffFactor must be a non-negative number');
  116. }
  117. }
  118. if (!validator.isNumber(retry.maxDelayInMillis) || retry.maxDelayInMillis < 0) {
  119. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_ARGUMENT, 'maxDelayInMillis must be a non-negative integer');
  120. }
  121. if (typeof retry.statusCodes !== 'undefined' && !validator.isArray(retry.statusCodes)) {
  122. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_ARGUMENT, 'statusCodes must be an array');
  123. }
  124. if (typeof retry.ioErrorCodes !== 'undefined' && !validator.isArray(retry.ioErrorCodes)) {
  125. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INVALID_ARGUMENT, 'ioErrorCodes must be an array');
  126. }
  127. }
  128. class HttpClient {
  129. constructor(retry = defaultRetryConfig()) {
  130. this.retry = retry;
  131. if (this.retry) {
  132. validateRetryConfig(this.retry);
  133. }
  134. }
  135. /**
  136. * Sends an HTTP request to a remote server. If the server responds with a successful response (2xx), the returned
  137. * promise resolves with an HttpResponse. If the server responds with an error (3xx, 4xx, 5xx), the promise rejects
  138. * with an HttpError. In case of all other errors, the promise rejects with a FirebaseAppError. If a request fails
  139. * due to a low-level network error, transparently retries the request once before rejecting the promise.
  140. *
  141. * If the request data is specified as an object, it will be serialized into a JSON string. The application/json
  142. * content-type header will also be automatically set in this case. For all other payload types, the content-type
  143. * header should be explicitly set by the caller. To send a JSON leaf value (e.g. "foo", 5), parse it into JSON,
  144. * and pass as a string or a Buffer along with the appropriate content-type header.
  145. *
  146. * @param config - HTTP request to be sent.
  147. * @returns A promise that resolves with the response details.
  148. */
  149. send(config) {
  150. return this.sendWithRetry(config);
  151. }
  152. /**
  153. * Sends an HTTP request. In the event of an error, retries the HTTP request according to the
  154. * RetryConfig set on the HttpClient.
  155. *
  156. * @param config - HTTP request to be sent.
  157. * @param retryAttempts - Number of retries performed up to now.
  158. * @returns A promise that resolves with the response details.
  159. */
  160. sendWithRetry(config, retryAttempts = 0) {
  161. return AsyncHttpCall.invoke(config)
  162. .then((resp) => {
  163. return this.createHttpResponse(resp);
  164. })
  165. .catch((err) => {
  166. const [delayMillis, canRetry] = this.getRetryDelayMillis(retryAttempts, err);
  167. if (canRetry && this.retry && delayMillis <= this.retry.maxDelayInMillis) {
  168. return this.waitForRetry(delayMillis).then(() => {
  169. return this.sendWithRetry(config, retryAttempts + 1);
  170. });
  171. }
  172. if (err.response) {
  173. throw new HttpError(this.createHttpResponse(err.response));
  174. }
  175. if (err.code === 'ETIMEDOUT') {
  176. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.NETWORK_TIMEOUT, `Error while making request: ${err.message}.`);
  177. }
  178. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.NETWORK_ERROR, `Error while making request: ${err.message}. Error code: ${err.code}`);
  179. });
  180. }
  181. createHttpResponse(resp) {
  182. if (resp.multipart) {
  183. return new MultipartHttpResponse(resp);
  184. }
  185. return new DefaultHttpResponse(resp);
  186. }
  187. waitForRetry(delayMillis) {
  188. if (delayMillis > 0) {
  189. return new Promise((resolve) => {
  190. setTimeout(resolve, delayMillis);
  191. });
  192. }
  193. return Promise.resolve();
  194. }
  195. /**
  196. * Checks if a failed request is eligible for a retry, and if so returns the duration to wait before initiating
  197. * the retry.
  198. *
  199. * @param retryAttempts - Number of retries completed up to now.
  200. * @param err - The last encountered error.
  201. * @returns A 2-tuple where the 1st element is the duration to wait before another retry, and the
  202. * 2nd element is a boolean indicating whether the request is eligible for a retry or not.
  203. */
  204. getRetryDelayMillis(retryAttempts, err) {
  205. if (!this.isRetryEligible(retryAttempts, err)) {
  206. return [0, false];
  207. }
  208. const response = err.response;
  209. if (response && response.headers['retry-after']) {
  210. const delayMillis = this.parseRetryAfterIntoMillis(response.headers['retry-after']);
  211. if (delayMillis > 0) {
  212. return [delayMillis, true];
  213. }
  214. }
  215. return [this.backOffDelayMillis(retryAttempts), true];
  216. }
  217. isRetryEligible(retryAttempts, err) {
  218. if (!this.retry) {
  219. return false;
  220. }
  221. if (retryAttempts >= this.retry.maxRetries) {
  222. return false;
  223. }
  224. if (err.response) {
  225. const statusCodes = this.retry.statusCodes || [];
  226. return statusCodes.indexOf(err.response.status) !== -1;
  227. }
  228. if (err.code) {
  229. const retryCodes = this.retry.ioErrorCodes || [];
  230. return retryCodes.indexOf(err.code) !== -1;
  231. }
  232. return false;
  233. }
  234. /**
  235. * Parses the Retry-After HTTP header as a milliseconds value. Return value is negative if the Retry-After header
  236. * contains an expired timestamp or otherwise malformed.
  237. */
  238. parseRetryAfterIntoMillis(retryAfter) {
  239. const delaySeconds = parseInt(retryAfter, 10);
  240. if (!isNaN(delaySeconds)) {
  241. return delaySeconds * 1000;
  242. }
  243. const date = new Date(retryAfter);
  244. if (!isNaN(date.getTime())) {
  245. return date.getTime() - Date.now();
  246. }
  247. return -1;
  248. }
  249. backOffDelayMillis(retryAttempts) {
  250. if (retryAttempts === 0) {
  251. return 0;
  252. }
  253. if (!this.retry) {
  254. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'Expected this.retry to exist.');
  255. }
  256. const backOffFactor = this.retry.backOffFactor || 0;
  257. const delayInSeconds = (2 ** retryAttempts) * backOffFactor;
  258. return Math.min(delayInSeconds * 1000, this.retry.maxDelayInMillis);
  259. }
  260. }
  261. exports.HttpClient = HttpClient;
  262. /**
  263. * Parses a full HTTP response message containing both a header and a body.
  264. *
  265. * @param response - The HTTP response to be parsed.
  266. * @param config - The request configuration that resulted in the HTTP response.
  267. * @returns An object containing the parsed HTTP status, headers and the body.
  268. */
  269. function parseHttpResponse(response, config) {
  270. const responseText = validator.isBuffer(response) ?
  271. response.toString('utf-8') : response;
  272. const endOfHeaderPos = responseText.indexOf('\r\n\r\n');
  273. const headerLines = responseText.substring(0, endOfHeaderPos).split('\r\n');
  274. const statusLine = headerLines[0];
  275. const status = statusLine.trim().split(/\s/)[1];
  276. const headers = {};
  277. headerLines.slice(1).forEach((line) => {
  278. const colonPos = line.indexOf(':');
  279. const name = line.substring(0, colonPos).trim().toLowerCase();
  280. const value = line.substring(colonPos + 1).trim();
  281. headers[name] = value;
  282. });
  283. let data = responseText.substring(endOfHeaderPos + 4);
  284. if (data.endsWith('\n')) {
  285. data = data.slice(0, -1);
  286. }
  287. if (data.endsWith('\r')) {
  288. data = data.slice(0, -1);
  289. }
  290. const lowLevelResponse = {
  291. status: parseInt(status, 10),
  292. headers,
  293. data,
  294. config,
  295. request: null,
  296. };
  297. if (!validator.isNumber(lowLevelResponse.status)) {
  298. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'Malformed HTTP status line.');
  299. }
  300. return new DefaultHttpResponse(lowLevelResponse);
  301. }
  302. exports.parseHttpResponse = parseHttpResponse;
  303. /**
  304. * A helper class for sending HTTP requests over the wire. This is a wrapper around the standard
  305. * http and https packages of Node.js, providing content processing, timeouts and error handling.
  306. * It also wraps the callback API of the Node.js standard library in a more flexible Promise API.
  307. */
  308. class AsyncHttpCall {
  309. /**
  310. * Sends an HTTP request based on the provided configuration.
  311. */
  312. static invoke(config) {
  313. return new AsyncHttpCall(config).promise;
  314. }
  315. constructor(config) {
  316. try {
  317. this.config = new HttpRequestConfigImpl(config);
  318. this.options = this.config.buildRequestOptions();
  319. this.entity = this.config.buildEntity(this.options.headers);
  320. this.promise = new Promise((resolve, reject) => {
  321. this.resolve = resolve;
  322. this.reject = reject;
  323. this.execute();
  324. });
  325. }
  326. catch (err) {
  327. this.promise = Promise.reject(this.enhanceError(err, null));
  328. }
  329. }
  330. execute() {
  331. const transport = this.options.protocol === 'https:' ? https : http;
  332. const req = transport.request(this.options, (res) => {
  333. this.handleResponse(res, req);
  334. });
  335. // Handle errors
  336. req.on('error', (err) => {
  337. if (req.aborted) {
  338. return;
  339. }
  340. this.enhanceAndReject(err, null, req);
  341. });
  342. const timeout = this.config.timeout;
  343. const timeoutCallback = () => {
  344. req.abort();
  345. this.rejectWithError(`timeout of ${timeout}ms exceeded`, 'ETIMEDOUT', req);
  346. };
  347. if (timeout) {
  348. // Listen to timeouts and throw an error.
  349. req.setTimeout(timeout, timeoutCallback);
  350. }
  351. // Send the request
  352. req.end(this.entity);
  353. }
  354. handleResponse(res, req) {
  355. if (req.aborted) {
  356. return;
  357. }
  358. if (!res.statusCode) {
  359. throw new error_1.FirebaseAppError(error_1.AppErrorCodes.INTERNAL_ERROR, 'Expected a statusCode on the response from a ClientRequest');
  360. }
  361. const response = {
  362. status: res.statusCode,
  363. headers: res.headers,
  364. request: req,
  365. data: undefined,
  366. config: this.config,
  367. };
  368. const boundary = this.getMultipartBoundary(res.headers);
  369. const respStream = this.uncompressResponse(res);
  370. if (boundary) {
  371. this.handleMultipartResponse(response, respStream, boundary);
  372. }
  373. else {
  374. this.handleRegularResponse(response, respStream);
  375. }
  376. }
  377. /**
  378. * Extracts multipart boundary from the HTTP header. The content-type header of a multipart
  379. * response has the form 'multipart/subtype; boundary=string'.
  380. *
  381. * If the content-type header does not exist, or does not start with
  382. * 'multipart/', then null will be returned.
  383. */
  384. getMultipartBoundary(headers) {
  385. const contentType = headers['content-type'];
  386. if (!contentType || !contentType.startsWith('multipart/')) {
  387. return null;
  388. }
  389. const segments = contentType.split(';');
  390. const emptyObject = {};
  391. const headerParams = segments.slice(1)
  392. .map((segment) => segment.trim().split('='))
  393. .reduce((curr, params) => {
  394. // Parse key=value pairs in the content-type header into properties of an object.
  395. if (params.length === 2) {
  396. const keyValuePair = {};
  397. keyValuePair[params[0]] = params[1];
  398. return Object.assign(curr, keyValuePair);
  399. }
  400. return curr;
  401. }, emptyObject);
  402. return headerParams.boundary;
  403. }
  404. uncompressResponse(res) {
  405. // Uncompress the response body transparently if required.
  406. let respStream = res;
  407. const encodings = ['gzip', 'compress', 'deflate'];
  408. if (res.headers['content-encoding'] && encodings.indexOf(res.headers['content-encoding']) !== -1) {
  409. // Add the unzipper to the body stream processing pipeline.
  410. const zlib = require('zlib'); // eslint-disable-line @typescript-eslint/no-var-requires
  411. respStream = respStream.pipe(zlib.createUnzip());
  412. // Remove the content-encoding in order to not confuse downstream operations.
  413. delete res.headers['content-encoding'];
  414. }
  415. return respStream;
  416. }
  417. handleMultipartResponse(response, respStream, boundary) {
  418. const busboy = require('@fastify/busboy'); // eslint-disable-line @typescript-eslint/no-var-requires
  419. const multipartParser = new busboy.Dicer({ boundary });
  420. const responseBuffer = [];
  421. multipartParser.on('part', (part) => {
  422. const tempBuffers = [];
  423. part.on('data', (partData) => {
  424. tempBuffers.push(partData);
  425. });
  426. part.on('end', () => {
  427. responseBuffer.push(Buffer.concat(tempBuffers));
  428. });
  429. });
  430. multipartParser.on('finish', () => {
  431. response.data = undefined;
  432. response.multipart = responseBuffer;
  433. this.finalizeResponse(response);
  434. });
  435. respStream.pipe(multipartParser);
  436. }
  437. handleRegularResponse(response, respStream) {
  438. const responseBuffer = [];
  439. respStream.on('data', (chunk) => {
  440. responseBuffer.push(chunk);
  441. });
  442. respStream.on('error', (err) => {
  443. const req = response.request;
  444. if (req && req.aborted) {
  445. return;
  446. }
  447. this.enhanceAndReject(err, null, req);
  448. });
  449. respStream.on('end', () => {
  450. response.data = Buffer.concat(responseBuffer).toString();
  451. this.finalizeResponse(response);
  452. });
  453. }
  454. /**
  455. * Finalizes the current HTTP call in-flight by either resolving or rejecting the associated
  456. * promise. In the event of an error, adds additional useful information to the returned error.
  457. */
  458. finalizeResponse(response) {
  459. if (response.status >= 200 && response.status < 300) {
  460. this.resolve(response);
  461. }
  462. else {
  463. this.rejectWithError('Request failed with status code ' + response.status, null, response.request, response);
  464. }
  465. }
  466. /**
  467. * Creates a new error from the given message, and enhances it with other information available.
  468. * Then the promise associated with this HTTP call is rejected with the resulting error.
  469. */
  470. rejectWithError(message, code, request, response) {
  471. const error = new Error(message);
  472. this.enhanceAndReject(error, code, request, response);
  473. }
  474. enhanceAndReject(error, code, request, response) {
  475. this.reject(this.enhanceError(error, code, request, response));
  476. }
  477. /**
  478. * Enhances the given error by adding more information to it. Specifically, the HttpRequestConfig,
  479. * the underlying request and response will be attached to the error.
  480. */
  481. enhanceError(error, code, request, response) {
  482. error.config = this.config;
  483. if (code) {
  484. error.code = code;
  485. }
  486. error.request = request;
  487. error.response = response;
  488. return error;
  489. }
  490. }
  491. /**
  492. * An adapter class for extracting options and entity data from an HttpRequestConfig.
  493. */
  494. class HttpRequestConfigImpl {
  495. constructor(config) {
  496. this.config = config;
  497. }
  498. get method() {
  499. return this.config.method;
  500. }
  501. get url() {
  502. return this.config.url;
  503. }
  504. get headers() {
  505. return this.config.headers;
  506. }
  507. get data() {
  508. return this.config.data;
  509. }
  510. get timeout() {
  511. return this.config.timeout;
  512. }
  513. get httpAgent() {
  514. return this.config.httpAgent;
  515. }
  516. buildRequestOptions() {
  517. const parsed = this.buildUrl();
  518. const protocol = parsed.protocol;
  519. let port = parsed.port;
  520. if (!port) {
  521. const isHttps = protocol === 'https:';
  522. port = isHttps ? '443' : '80';
  523. }
  524. return {
  525. protocol,
  526. hostname: parsed.hostname,
  527. port,
  528. path: parsed.path,
  529. method: this.method,
  530. agent: this.httpAgent,
  531. headers: Object.assign({}, this.headers),
  532. };
  533. }
  534. buildEntity(headers) {
  535. let data;
  536. if (!this.hasEntity() || !this.isEntityEnclosingRequest()) {
  537. return data;
  538. }
  539. if (validator.isBuffer(this.data)) {
  540. data = this.data;
  541. }
  542. else if (validator.isObject(this.data)) {
  543. data = Buffer.from(JSON.stringify(this.data), 'utf-8');
  544. if (typeof headers['content-type'] === 'undefined') {
  545. headers['content-type'] = 'application/json;charset=utf-8';
  546. }
  547. }
  548. else if (validator.isString(this.data)) {
  549. data = Buffer.from(this.data, 'utf-8');
  550. }
  551. else {
  552. throw new Error('Request data must be a string, a Buffer or a json serializable object');
  553. }
  554. // Add Content-Length header if data exists.
  555. headers['Content-Length'] = data.length.toString();
  556. return data;
  557. }
  558. buildUrl() {
  559. const fullUrl = this.urlWithProtocol();
  560. if (!this.hasEntity() || this.isEntityEnclosingRequest()) {
  561. return url.parse(fullUrl);
  562. }
  563. if (!validator.isObject(this.data)) {
  564. throw new Error(`${this.method} requests cannot have a body`);
  565. }
  566. // Parse URL and append data to query string.
  567. const parsedUrl = new url.URL(fullUrl);
  568. const dataObj = this.data;
  569. for (const key in dataObj) {
  570. if (Object.prototype.hasOwnProperty.call(dataObj, key)) {
  571. parsedUrl.searchParams.append(key, dataObj[key]);
  572. }
  573. }
  574. return url.parse(parsedUrl.toString());
  575. }
  576. urlWithProtocol() {
  577. const fullUrl = this.url;
  578. if (fullUrl.startsWith('http://') || fullUrl.startsWith('https://')) {
  579. return fullUrl;
  580. }
  581. return `https://${fullUrl}`;
  582. }
  583. hasEntity() {
  584. return !!this.data;
  585. }
  586. isEntityEnclosingRequest() {
  587. // GET and HEAD requests do not support entity (body) in request.
  588. return this.method !== 'GET' && this.method !== 'HEAD';
  589. }
  590. }
  591. class AuthorizedHttpClient extends HttpClient {
  592. constructor(app) {
  593. super();
  594. this.app = app;
  595. }
  596. send(request) {
  597. return this.getToken().then((token) => {
  598. const requestCopy = Object.assign({}, request);
  599. requestCopy.headers = Object.assign({}, request.headers);
  600. const authHeader = 'Authorization';
  601. requestCopy.headers[authHeader] = `Bearer ${token}`;
  602. if (!requestCopy.httpAgent && this.app.options.httpAgent) {
  603. requestCopy.httpAgent = this.app.options.httpAgent;
  604. }
  605. return super.send(requestCopy);
  606. });
  607. }
  608. getToken() {
  609. return this.app.INTERNAL.getToken()
  610. .then((accessTokenObj) => {
  611. return accessTokenObj.accessToken;
  612. });
  613. }
  614. }
  615. exports.AuthorizedHttpClient = AuthorizedHttpClient;
  616. /**
  617. * Class that defines all the settings for the backend API endpoint.
  618. *
  619. * @param endpoint - The Firebase Auth backend endpoint.
  620. * @param httpMethod - The http method for that endpoint.
  621. * @constructor
  622. */
  623. class ApiSettings {
  624. constructor(endpoint, httpMethod = 'POST') {
  625. this.endpoint = endpoint;
  626. this.httpMethod = httpMethod;
  627. this.setRequestValidator(null)
  628. .setResponseValidator(null);
  629. }
  630. /** @returns The backend API endpoint. */
  631. getEndpoint() {
  632. return this.endpoint;
  633. }
  634. /** @returns The request HTTP method. */
  635. getHttpMethod() {
  636. return this.httpMethod;
  637. }
  638. /**
  639. * @param requestValidator - The request validator.
  640. * @returns The current API settings instance.
  641. */
  642. setRequestValidator(requestValidator) {
  643. const nullFunction = () => undefined;
  644. this.requestValidator = requestValidator || nullFunction;
  645. return this;
  646. }
  647. /** @returns The request validator. */
  648. getRequestValidator() {
  649. return this.requestValidator;
  650. }
  651. /**
  652. * @param responseValidator - The response validator.
  653. * @returns The current API settings instance.
  654. */
  655. setResponseValidator(responseValidator) {
  656. const nullFunction = () => undefined;
  657. this.responseValidator = responseValidator || nullFunction;
  658. return this;
  659. }
  660. /** @returns The response validator. */
  661. getResponseValidator() {
  662. return this.responseValidator;
  663. }
  664. }
  665. exports.ApiSettings = ApiSettings;
  666. /**
  667. * Class used for polling an endpoint with exponential backoff.
  668. *
  669. * Example usage:
  670. * ```
  671. * const poller = new ExponentialBackoffPoller();
  672. * poller
  673. * .poll(() => {
  674. * return myRequestToPoll()
  675. * .then((responseData: any) => {
  676. * if (!isValid(responseData)) {
  677. * // Continue polling.
  678. * return null;
  679. * }
  680. *
  681. * // Polling complete. Resolve promise with final response data.
  682. * return responseData;
  683. * });
  684. * })
  685. * .then((responseData: any) => {
  686. * console.log(`Final response: ${responseData}`);
  687. * });
  688. * ```
  689. */
  690. class ExponentialBackoffPoller extends events_1.EventEmitter {
  691. constructor(initialPollingDelayMillis = 1000, maxPollingDelayMillis = 10000, masterTimeoutMillis = 60000) {
  692. super();
  693. this.initialPollingDelayMillis = initialPollingDelayMillis;
  694. this.maxPollingDelayMillis = maxPollingDelayMillis;
  695. this.masterTimeoutMillis = masterTimeoutMillis;
  696. this.numTries = 0;
  697. this.completed = false;
  698. }
  699. /**
  700. * Poll the provided callback with exponential backoff.
  701. *
  702. * @param callback - The callback to be called for each poll. If the
  703. * callback resolves to a falsey value, polling will continue. Otherwise, the truthy
  704. * resolution will be used to resolve the promise returned by this method.
  705. * @returns A Promise which resolves to the truthy value returned by the provided
  706. * callback when polling is complete.
  707. */
  708. poll(callback) {
  709. if (this.pollCallback) {
  710. throw new Error('poll() can only be called once per instance of ExponentialBackoffPoller');
  711. }
  712. this.pollCallback = callback;
  713. this.on('poll', this.repoll);
  714. this.masterTimer = setTimeout(() => {
  715. if (this.completed) {
  716. return;
  717. }
  718. this.markCompleted();
  719. this.reject(new Error('ExponentialBackoffPoller deadline exceeded - Master timeout reached'));
  720. }, this.masterTimeoutMillis);
  721. return new Promise((resolve, reject) => {
  722. this.resolve = resolve;
  723. this.reject = reject;
  724. this.repoll();
  725. });
  726. }
  727. repoll() {
  728. this.pollCallback()
  729. .then((result) => {
  730. if (this.completed) {
  731. return;
  732. }
  733. if (!result) {
  734. this.repollTimer =
  735. setTimeout(() => this.emit('poll'), this.getPollingDelayMillis());
  736. this.numTries++;
  737. return;
  738. }
  739. this.markCompleted();
  740. this.resolve(result);
  741. })
  742. .catch((err) => {
  743. if (this.completed) {
  744. return;
  745. }
  746. this.markCompleted();
  747. this.reject(err);
  748. });
  749. }
  750. getPollingDelayMillis() {
  751. const increasedPollingDelay = Math.pow(2, this.numTries) * this.initialPollingDelayMillis;
  752. return Math.min(increasedPollingDelay, this.maxPollingDelayMillis);
  753. }
  754. markCompleted() {
  755. this.completed = true;
  756. if (this.masterTimer) {
  757. clearTimeout(this.masterTimer);
  758. }
  759. if (this.repollTimer) {
  760. clearTimeout(this.repollTimer);
  761. }
  762. }
  763. }
  764. exports.ExponentialBackoffPoller = ExponentialBackoffPoller;