PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-api/src

Просмотр файла: api.ts

/**
 * @prettier
 */
import Debug from 'debug';
import eol from 'eol';
import _ from 'lodash';
import sanitizeHtml from 'sanitize-html';
import superagent from 'superagent';
import urlLib from 'url';
import querystring from 'querystring';

import { ApiResponseError, BitGoRequest } from '@bitgo/sdk-core';

import { AuthVersion, VerifyResponseOptions } from './types';
import { BitGoAPI } from './bitgoAPI';

const debug = Debug('bitgo:api');

/**
 * Add the bitgo-specific result() function on a superagent request.
 *
 * If the server response is successful, the `result()` function will return either the entire response body,
 * or the field from the response body specified by the `optionalField` parameter if it is provided.
 *
 * If the server response with an error, `result()` will handle HTTP errors appropriately by
 * rethrowing them as an `ApiResponseError` if possible, and otherwise rethrowing the underlying response error.
 *
 * @param req
 */
export function toBitgoRequest<ResponseResultType = any>(
  req: superagent.SuperAgentRequest
): BitGoRequest<ResponseResultType> {
  return Object.assign(req, {
    result(optionalField?: string) {
      return req.then(
        (response) => handleResponseResult<ResponseResultType>(optionalField)(response),
        (error) => handleResponseError(error)
      );
    },
  });
}

/**
 * Return a function which extracts the specified response body property from the response if successful,
 * otherwise throw an `ApiErrorResponse` parsed from the response body.
 * @param optionalField
 */
export function handleResponseResult<ResponseResultType>(
  optionalField?: string
): (res: superagent.Response) => ResponseResultType {
  return function (res: superagent.Response): ResponseResultType {
    if (_.isNumber(res.status) && res.status >= 200 && res.status < 300) {
      return (
        // If there's an optional field and the body is non-nullish with that property, return it;
        // otherwise return the body if available; if not, return the text; and finally fallback to the entire response.
        (optionalField && res.body && res.body[optionalField] !== undefined ? res.body[optionalField] : res.body) ??
        res.text ??
        res
      );
    }
    throw errFromResponse(res);
  };
}

/**
 * Extract relevant information from a successful response (that is, a response with an HTTP status code
 * between 200 and 299), but which resulted in an application specific error and use it to construct and
 * throw an `ApiErrorResponse`.
 *
 * @param res
 */
function errFromResponse<ResponseBodyType>(res: superagent.Response): ApiResponseError {
  const message = createResponseErrorString(res);
  const status = res.status;
  const result = res.body as ResponseBodyType;
  const invalidToken = _.has(res.header, 'x-auth-required') && res.header['x-auth-required'] === 'true';
  const needsOtp = res.body?.needsOTP !== undefined;
  return new ApiResponseError(message, status, result, invalidToken, needsOtp);
}

/**
 * Handle an error or an error containing an HTTP response and use it to throw a well-formed error object.
 *
 * @param e
 */
export function handleResponseError(e: Error & { response?: superagent.Response }): never {
  if (e.response) {
    throw errFromResponse(e.response);
  }
  throw e;
}

/**
 * There are many ways a request can fail, and may ways information on that failure can be
 * communicated to the client. This function tries to handle those cases and create a sane error string
 * @param res Response from an HTTP request
 */
function createResponseErrorString(res: superagent.Response): string {
  let errString = res.status.toString(); // at the very least we'll have the status code
  if (res.body?.error) {
    // this is the case we hope for, where the server gives us a nice error from the JSON body
    errString = res.body.error;
  } else if (res.text) {
    // if the response came back as text, we try to parse it as HTML and remove all tags, leaving us
    // just the bare text, which we then trim of excessive newlines and limit to a certain length
    try {
      let sanitizedText = sanitizeHtml(res.text, { allowedTags: [] });
      sanitizedText = sanitizedText.trim();
      sanitizedText = eol.lf(sanitizedText); // use '\n' for all newlines
      sanitizedText = _.replace(sanitizedText, /\n[ |\t]{1,}\n/g, '\n\n'); // remove the spaces/tabs between newlines
      sanitizedText = _.replace(sanitizedText, /[\n]{3,}/g, '\n\n'); // have at most 2 consecutive newlines
      sanitizedText = sanitizedText.substring(0, 5000); // prevent message from getting too large
      errString = errString + '\n' + sanitizedText; // add it to our existing errString (at this point the more info the better!)
    } catch (e) {
      // do nothing, the response's HTML was too wacky to be parsed cleanly
      debug('got error with message "%s" while creating response error string from response: %s', e.message, res.text);
    }
  }

  return errString;
}

/**
 * Serialize request data based on the request content type
 * Note: Not sure this is still needed or even useful. Consider removing.
 * @param req
 */
export function serializeRequestData(req: superagent.Request): string | undefined {
  let data: string | Record<string, unknown> = (req as any)._data;
  if (typeof data !== 'string') {
    let contentType = req.get('Content-Type');
    // Parse out just the content type from the header (ignore the charset)
    if (contentType) {
      contentType = contentType.split(';')[0];
    }
    let serialize = superagent.serialize[contentType];
    if (!serialize && /[\/+]json\b/.test(contentType)) {
      serialize = superagent.serialize['application/json'];
    }
    if (serialize) {
      data = serialize(data);
      (req as any)._data = data;
      return data;
    }
  }
}

/**
 * Set the superagent query string correctly for browsers or node.
 * @param req
 */
export function setRequestQueryString(req: superagent.SuperAgentRequest): void {
  const urlDetails = urlLib.parse(req.url);

  let queryString: string | undefined;
  const query: string[] = (req as any)._query;
  const qs: { [key: string]: string } = (req as any).qs;
  if (query && query.length > 0) {
    // browser version
    queryString = query.join('&');
    (req as any)._query = [];
  } else if (qs) {
    // node version
    queryString = querystring.stringify(qs);
    (req as any).qs = null;
  }

  if (queryString) {
    if (urlDetails.search) {
      urlDetails.search += '&' + queryString;
    } else {
      urlDetails.search = '?' + queryString;
    }
    req.url = urlLib.format(urlDetails);
  }
}

/**
 * Verify that the response received from the server is signed correctly.
 * Right now, it is very permissive with the timestamp variance.
 */
export function verifyResponse(
  bitgo: BitGoAPI,
  token: string | undefined,
  method: VerifyResponseOptions['method'],
  req: superagent.SuperAgentRequest,
  response: superagent.Response,
  authVersion: AuthVersion
): superagent.Response {
  // we can't verify the response if we're not authenticated
  if (!req.isV2Authenticated || !req.authenticationToken) {
    return response;
  }

  const verificationResponse = bitgo.verifyResponse({
    url: req.url,
    hmac: response.header.hmac,
    statusCode: response.status,
    text: response.text,
    timestamp: response.header.timestamp,
    token: req.authenticationToken,
    method,
    authVersion,
  });

  if (!verificationResponse.isValid) {
    // calculate the HMAC
    const receivedHmac = response.header.hmac;
    const expectedHmac = verificationResponse.expectedHmac;
    const signatureSubject = verificationResponse.signatureSubject;
    // Log only the first 10 characters of the token to ensure the full token isn't logged.
    const partialBitgoToken = token ? token.substring(0, 10) : '';
    const errorDetails = {
      expectedHmac,
      receivedHmac,
      hmacInput: signatureSubject,
      requestToken: req.authenticationToken,
      bitgoToken: partialBitgoToken,
    };
    debug('Invalid response HMAC: %O', errorDetails);
    throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, errorDetails);
  }

  if (bitgo.getAuthVersion() === 3 && !verificationResponse.isInResponseValidityWindow) {
    const errorDetails = {
      timestamp: response.header.timestamp,
      verificationTime: verificationResponse.verificationTime,
    };
    debug('Server response outside response validity time window: %O', errorDetails);
    throw new ApiResponseError(
      'server response outside response validity time window, possible man-in-the-middle-attack',
      511,
      errorDetails
    );
  }
  return response;
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!