import { APIDetails , getRequestHeaders , initAPIDetails , SDK_VERSION } from '@sentry/core' ; import { DsnProtocol , Event , Response , SentryRequest , SentryRequestType , Session , SessionAggregates , Transport , TransportOptions , } from '@sentry/types' ; import { eventStatusFromHttpCode , logger , makePromiseBuffer , parseRetryAfterHeader , PromiseBuffer , SentryError , } from '@sentry/utils' ; import * as fs from 'fs' ; import * as http from 'http' ; import * as https from 'https' ; import { URL } from 'url' ; import { SDK_NAME } from '../../version' ; import { HTTPModule } from './http-module' ; export type URLParts = Pick < URL , 'hostname' | 'pathname' | 'port' | 'protocol' > ; export type UrlParser = ( url : string ) => URLParts ; const CATEGORY_MAPPING = { event : 'error' , transaction : 'transaction' , session : 'session' , attachment : 'attachment' , } ; /** Base Transport class implementation */ export abstract class BaseTransport implements Transport { /** The Agent used for corresponding transport */ public module ? : HTTPModule ; /** The Agent used for corresponding transport */ public client ? : http . Agent | https . Agent ; /** API object */ protected _api : APIDetails ; /** A simple buffer holding all requests. */ protected readonly _buffer : PromiseBuffer < Response > = makePromiseBuffer ( 30 ) ; /** Locks transport after receiving rate limits in a response */ protected readonly _rateLimits : Record < string , Date > = { } ; /** Create instance and set this.dsn */ public constructor ( public options ) { this . _api = initAPIDetails ( options . dsn , options . _metadata , options . tunnel ) ; } /** Default function used to parse URLs */ public urlParser : UrlParser = url => new URL ( url ) ; inheritDoc public sendEvent ( _ ) { throw new SentryError ( 'Transport Class has to implement `sendEvent` method.' ) ; } inheritDoc public close ( timeout ? ) { return this . _buffer . drain ( timeout ) ; } /**
     * Extracts proxy settings from client options and env variables.
     *
     * Honors `no_proxy` env variable with the highest priority to allow for hosts exclusion.
     *
     * An order of priority for available protocols is:
     * `http`  => `options.httpProxy` | `process.env.http_proxy`
     * `https` => `options.httpsProxy` | `options.httpProxy` | `process.env.https_proxy` | `process.env.http_proxy`
     */ protected _getProxy ( protocol ) { const { no_proxy , http_proxy , https_proxy } = process . env ; const { httpProxy , httpsProxy } = this . options ; const proxy = protocol === 'http' ? httpProxy || http_proxy : httpsProxy || httpProxy || https_proxy || http_proxy ; if ( ! no_proxy ) { return proxy ; } const { host , port } = this . _api . dsn ; for ( const np of no_proxy . split ( ',' ) ) { if ( host . endsWith ( np ) || `${ host }:${ port }` . endsWith ( np ) ) { return ; } } return proxy ; } /** Returns a build request option object used by request */ protected _getRequestOptions ( urlParts ) { const headers = { ... getRequestHeaders ( this . _api . dsn , SDK_NAME , SDK_VERSION ) , ... this . options . headers , } ; const { hostname , pathname , port , protocol } = urlParts ; const path = `${ pathname }` ; return { agent : this . client , headers , hostname , method : 'POST' , path , port , protocol , ... ( this . options . caCerts && { ca : fs . readFileSync ( this . options . caCerts ) , } ) , } ; } /**
     * Gets the time that given category is disabled until for rate limiting
     */ protected _disabledUntil ( requestType ) { const category = CATEGORY_MAPPING [ requestType ] ; return this . _rateLimits [ category ] || this . _rateLimits . all ; } /**
     * Checks if a category is rate limited
     */ protected _isRateLimited ( requestType ) { return this . _disabledUntil ( requestType ) > new Date ( Date . now ( ) ) ; } /**
     * Sets internal _rateLimits from incoming headers. Returns true if headers contains a non-empty rate limiting header.
     */ protected _handleRateLimit ( headers ) { const now = Date . now ( ) ; const rlHeader = headers [ 'x-sentry-rate-limits' ] ; const raHeader = headers [ 'retry-after' ] ; if ( rlHeader ) { for ( const limit of rlHeader . trim ( ) . split ( ',' ) ) { const parameters = limit . split ( ':' , 2 ) ; const headerDelay = parseInt ( parameters [ 0 ] , 10 ) ; const delay = ( ! isNaN ( headerDelay ) ? headerDelay : 60 ) * 1000 ; for ( const category of ( parameters [ 1 ] && parameters [ 1 ] . split ( ';' ) ) || [ 'all' ] ) { const categoriesAllowed = [ ... ( Object . keys ( CATEGORY_MAPPING ) as [ SentryRequestType ] ) . map ( k => CATEGORY_MAPPING [ k ] ) , 'all' , ] ; if ( categoriesAllowed . includes ( category ) ) this . _rateLimits [ category ] = new Date ( now + delay ) ; } } return true ; } else if ( raHeader ) { this . _rateLimits . all = new Date ( now + parseRetryAfterHeader ( now , raHeader ) ) ; return true ; } return false ; } /** JSDoc */ protected async _send ( sentryRequest , originalPayload ? ) { if ( ! this . module ) { throw new SentryError ( 'No module available' ) ; } if ( originalPayload && this . _isRateLimited ( sentryRequest . type ) ) { return Promise . reject ( { payload : originalPayload , type : sentryRequest . type , reason : `Transport for ${ sentryRequest . type } requests locked till ${ this . _disabledUntil ( sentryRequest . type ) } due to too many requests.` , status : 429 , } ) ; } return this . _buffer . add ( ( ) => new Promise < Response > ( ( resolve , reject ) => { if ( ! this . module ) { throw new SentryError ( 'No module available' ) ; } const options = this . _getRequestOptions ( this . urlParser ( sentryRequest . url ) ) ; const req = this . module . request ( options , res => { const statusCode = res . statusCode || 500 ; const status = eventStatusFromHttpCode ( statusCode ) ; res . setEncoding ( 'utf8' ) ; /**
                 * "Key-value pairs of header names and values. Header names are lower-cased."
                 * https://nodejs.org/api/http.html#http_message_headers
                 */ let retryAfterHeader = res . headers ? res . headers [ 'retry-after' ] : '' ; retryAfterHeader = ( Array . isArray ( retryAfterHeader ) ? retryAfterHeader [ 0 ] : retryAfterHeader ) as string ; let rlHeader = res . headers ? res . headers [ 'x-sentry-rate-limits' ] : '' ; rlHeader = ( Array . isArray ( rlHeader ) ? rlHeader [ 0 ] : rlHeader ) as string ; const headers = { 'x-sentry-rate-limits' : rlHeader , 'retry-after' : retryAfterHeader , } ; const limited = this . _handleRateLimit ( headers ) ; if ( limited ) logger . warn ( `Too many ${ sentryRequest . type } requests, backing off until: ${ this . _disabledUntil ( sentryRequest . type ) }` ) ; if ( status === 'success' ) { resolve ( { status } ) ; } else { let rejectionMessage = `HTTP Error (${ statusCode })` ; if ( res . headers && res . headers [ 'x-sentry-error' ] ) { rejectionMessage += `: ${ res . headers [ 'x-sentry-error' ] }` ; } reject ( new SentryError ( rejectionMessage ) ) ; } res . on ( 'data' , ( ) => { } ) ; res . on ( 'end' , ( ) => { } ) ; } ) ; req . on ( 'error' , reject ) ; req . end ( sentryRequest . body ) ; } ) ) ; } }