UNPKG

tlsclientwrapper

Version:

A wrapper for `bogdanfinn/tls-client` based on koffi for unparalleled performance and usability. Inspired by @dryft/tlsclient

806 lines (737 loc) 25.9 kB
import ModuleClient from './utils/client.js'; import crypto from 'node:crypto'; import type { Piscina } from 'piscina'; // Type definitions export type ChromeProfile = | 'chrome_103' | 'chrome_104' | 'chrome_105' | 'chrome_106' | 'chrome_107' | 'chrome_108' | 'chrome_109' | 'chrome_110' | 'chrome_111' | 'chrome_112' | 'chrome_116_PSK' | 'chrome_116_PSK_PQ' | 'chrome_117' | 'chrome_120' | 'chrome_124' | 'chrome_131' | 'chrome_131_PSK' | 'chrome_133' | 'chrome_133_PSK'; export type SafariProfile = 'safari_15_6_1' | 'safari_16_0'; export type SafariIOSProfile = | 'safari_ios_15_5' | 'safari_ios_15_6' | 'safari_ios_16_0' | 'safari_ios_17_0' | 'safari_ios_18_0' | 'safari_ios_18_5'; export type SafariIpadOSProfile = 'safari_ios_15_6'; export type FirefoxProfile = | 'firefox_102' | 'firefox_104' | 'firefox_105' | 'firefox_106' | 'firefox_108' | 'firefox_110' | 'firefox_117' | 'firefox_120' | 'firefox_123' | 'firefox_132' | 'firefox_133' | 'firefox_135'; export type OperaProfile = 'opera_89' | 'opera_90' | 'opera_91'; export type CustomClientProfile = | 'zalando_ios_mobile' | 'nike_ios_mobile' | 'cloudscraper' | 'mms_ios' | 'mms_ios_1' | 'mms_ios_2' | 'mms_ios_3' | 'mesh_ios' | 'mesh_ios_1' | 'confirmed_ios'; export type ClientProfile = | ChromeProfile | SafariProfile | SafariIOSProfile | SafariIpadOSProfile | FirefoxProfile | OperaProfile | CustomClientProfile; /** * Certificate compression algorithm types */ export type CertCompressionAlgorithm = 'zlib' | 'brotli' | 'zstd'; /** * HTTP/2 settings keys */ export type H2SettingsKey = | 'HEADER_TABLE_SIZE' | 'ENABLE_PUSH' | 'MAX_CONCURRENT_STREAMS' | 'INITIAL_WINDOW_SIZE' | 'MAX_FRAME_SIZE' | 'MAX_HEADER_LIST_SIZE' | 'UNKNOWN_SETTING_7' | 'UNKNOWN_SETTING_8' | 'UNKNOWN_SETTING_9'; /** * Supported TLS versions */ export type SupportedVersion = 'GREASE' | '1.3' | '1.2' | '1.1' | '1.0'; /** * Supported signature algorithms */ export type SignatureAlgorithm = | 'PKCS1WithSHA256' | 'PKCS1WithSHA384' | 'PKCS1WithSHA512' | 'PSSWithSHA256' | 'PSSWithSHA384' | 'PSSWithSHA512' | 'ECDSAWithP256AndSHA256' | 'ECDSAWithP384AndSHA384' | 'ECDSAWithP521AndSHA512' | 'PKCS1WithSHA1' | 'ECDSAWithSHA1' | 'Ed25519' | 'SHA224_RSA' | 'SHA224_ECDSA'; /** * Key share curves */ export type KeyShareCurve = | 'GREASE' | 'P256' | 'P384' | 'P521' | 'X25519' | 'P256Kyber768' | 'X25519Kyber512D' | 'X25519Kyber768' | 'X25519MLKEM768'; /** * KDF identifiers */ export type KdfId = 'HKDF_SHA256' | 'HKDF_SHA384' | 'HKDF_SHA512'; /** * AEAD identifiers */ export type AeadId = 'AEAD_AES_128_GCM' | 'AEAD_AES_256_GCM' | 'AEAD_CHACHA20_POLY1305'; /** * Certificate pinning hosts configuration * @example { "example.com": ["sha256/AAAAAAAAAAAAAAAAAAAAAA=="] } */ export interface CertificatePinningHosts { [hostname: string]: string[]; } /** * HTTP/2 stream priority parameters */ export interface PriorityParam { streamDep?: number; exclusive?: boolean; weight?: number; } /** * HTTP/2 priority frame configuration */ export interface PriorityFrames { streamID: number; priorityParam: PriorityParam; } /** * ECH Candidate Cipher Suite configuration */ export interface CanidateCipherSuite { kdfId: KdfId; aeadId: AeadId; } /** * Custom TLS client configuration for advanced fingerprint customization */ export interface CustomTLSClient { /** Certificate compression algorithm */ certCompressionAlgo?: CertCompressionAlgorithm; /** Connection flow identifier */ connectionFlow?: number; /** HTTP/2 settings map */ h2Settings?: Record<H2SettingsKey, number>; /** Array of H2Settings keys in order */ h2SettingsOrder?: H2SettingsKey[]; /** Priority parameters for headers */ headerPriority?: PriorityParam; /** JA3 fingerprint string */ ja3String?: string; /** Key share curves */ keyShareCurves?: KeyShareCurve[]; /** Array of priority frames configuration */ priorityFrames?: PriorityFrames[]; /** List of supported protocols for the ALPN Extension */ alpnProtocols?: string[]; /** List of supported protocols for the ALPS Extension */ alpsProtocols?: string[]; /** List of ECH Candidate Payloads */ ECHCandidatePayloads?: number[]; /** ECH Candidate Cipher Suites */ ECHCandidateCipherSuites?: CanidateCipherSuite[]; /** Order of pseudo headers */ pseudoHeaderOrder?: string[]; /** Supported algorithms for delegated credentials */ supportedDelegatedCredentialsAlgorithms?: SignatureAlgorithm[]; /** Supported signature algorithms */ supportedSignatureAlgorithms?: SignatureAlgorithm[]; /** Supported TLS versions */ supportedVersions?: SupportedVersion[]; } /** * HTTP transport configuration options */ export interface TransportOptions { /** If true, keep-alives will be disabled */ disableKeepAlives?: boolean; /** If true, compression will be disabled */ disableCompression?: boolean; /** Maximum number of idle connections */ maxIdleConns?: number; /** Maximum number of idle connections per host */ maxIdleConnsPerHost?: number; /** Maximum number of connections per host */ maxConnsPerHost?: number; /** Maximum number of response header bytes */ maxResponseHeaderBytes?: number; /** Write buffer size */ writeBufferSize?: number; /** Read buffer size */ readBufferSize?: number; /** Idle connection timeout */ idleConnTimeout?: number; } /** * HTTP Cookie representation */ export interface Cookie { /** The domain of the cookie */ domain: string; /** The expiration time of the cookie (Unix timestamp) */ expires: number; /** Number of seconds the cookie is valid. If both expires and maxAge are set, maxAge has precedence */ maxAge?: number; /** The name of the cookie */ name: string; /** The path of the cookie */ path: string; /** The value of the cookie */ value: string; } /** * Default options for TLS client configuration */ export interface TlsClientDefaultOptions { /** Identifier of the TLS client (default: 'chrome_133') */ tlsClientIdentifier?: ClientProfile; /** If true, wrapper will retry the request based on retryStatusCodes (default: true) */ retryIsEnabled?: boolean; /** Maximum number of retries (default: 3) */ retryMaxCount?: number; /** Status codes for retries (default: [408, 429, 500, 502, 503, 504, 521, 522, 523, 524]) */ retryStatusCodes?: number[]; /** If true, panics will be caught (default: false) */ catchPanics?: boolean; /** Hosts for certificate pinning */ certificatePinningHosts?: CertificatePinningHosts | null; /** Custom TLS client configuration */ customTlsClient?: CustomTLSClient | null; /** Transport options */ transportOptions?: TransportOptions | null; /** If true, redirects will be followed (default: false) */ followRedirects?: boolean; /** If true, HTTP/1 will be forced (default: false) */ forceHttp1?: boolean; /** If true, HTTP/3 will be disabled (default: false) */ disableHttp3?: boolean; /** Order of headers */ headerOrder?: string[]; /** Default headers which will be used in every request */ defaultHeaders?: Record<string, string> | null; /** Headers to be used during the CONNECT request */ connectHeaders?: Record<string, string> | null; /** If true, insecure verification will be skipped (default: false) */ insecureSkipVerify?: boolean; /** If true, the request is a byte request (default: false) */ isByteRequest?: boolean; /** If true, the response is a byte response (default: false) */ isByteResponse?: boolean; /** If true, the proxy is rotating (default: false) */ isRotatingProxy?: boolean; /** URL of the proxy. Example: http://user:password@ip:port */ proxyUrl?: string | null; /** Default cookies for requests */ defaultCookies?: Cookie[] | null; /** Override the request host */ requestHostOverride?: string | null; /** If true, IPV6 will be disabled (default: false) */ disableIPV6?: boolean; /** If true, IPV4 will be disabled (default: false) */ disableIPV4?: boolean; /** Local address [not Sure? Docs are not clear] */ localAddress?: string | null; /** Server name overwrite. See: https://bogdanfinn.gitbook.io/open-source-oasis/tls-client/client-options */ serverNameOverwrite?: string; /** Block size of the stream output */ streamOutputBlockSize?: number | null; /** EOF symbol of the stream output */ streamOutputEOFSymbol?: string | null; /** Path of the stream output */ streamOutputPath?: string | null; /** Timeout in milliseconds (default: 0) */ timeoutMilliseconds?: number; /** Timeout in seconds (default: 60) */ timeoutSeconds?: number; /** If true, debug mode is enabled (default: false) */ withDebug?: boolean; /** If true, the default cookie jar is used (default: true) */ withDefaultCookieJar?: boolean; /** If true, the cookie jar is not used (default: false) */ withoutCookieJar?: boolean; /** If true, the order of TLS extensions is randomized (default: true) */ withRandomTLSExtensionOrder?: boolean; /** Custom path to download the TLS library */ customLibraryDownloadPath?: string | null; } /** * Options for individual TLS client requests * Custom configurable options for the TLS client */ export interface TlsClientOptions extends Omit<TlsClientDefaultOptions, 'defaultHeaders' | 'defaultCookies'> { /** Headers for this specific request */ headers?: Record<string, string> | null; /** Body of the request */ requestBody?: string | null; /** Cookies for this specific request */ requestCookies?: Cookie[] | null; /** ID of the session [not recommended to use, use the SessionClient instead] */ sessionId?: string | null; /** The target URL */ requestUrl?: string; /** HTTP method (GET, POST, etc.) - internal use only */ requestMethod?: string; } /** * Response from TLS client request */ export interface TlsClientResponse { /** The reusable sessionId if provided on the request */ sessionId: string; /** The status code of the response */ status: number; /** The target URL of the request */ target: string; /** The response body as a string, or the error message */ body: string; /** The headers of the response */ headers: Record<string, string>; /** The cookies of the response */ cookies: Record<string, Cookie>; /** The number of retries */ retryCount: number; } /** * Input for getting cookies from a session */ export interface GetCookiesInput { /** The existing session ID */ sessionId: string; /** The URL to get cookies for */ url: string; } /** * Input for adding cookies to a session */ export interface AddCookiesInput { /** The existing session ID */ sessionId: string; /** The URL to add cookies for */ url: string; /** The cookies to add */ cookies: Cookie[] | null; } /** * Response containing cookies */ export interface CookieResponse { /** The cookies from the response */ cookies: Cookie[] | null; } /** * SessionClient class for managing TLS client sessions */ export class SessionClient { public defaultOptions: TlsClientDefaultOptions; private readonly sessionId: string; private readonly moduleClient: ModuleClient; private pool: Piscina | null = null; /** * @description Create a new SessionClient * @param {ModuleClient} moduleClient - The shared ModuleClient instance * @param {TlsClientDefaultOptions} [options={}] - SessionClient options */ constructor(moduleClient: ModuleClient, options: TlsClientDefaultOptions = {}) { if (!moduleClient) { throw new Error( 'ModuleClient must be provided. Please create a new ModuleClient instance and pass it as the first argument.' ); } if (!(moduleClient instanceof ModuleClient)) { throw new Error('ModuleClient must be an instance of ModuleClient'); } this.defaultOptions = { tlsClientIdentifier: 'chrome_133', catchPanics: false, certificatePinningHosts: null, customTlsClient: null, customLibraryDownloadPath: null, transportOptions: null, followRedirects: false, forceHttp1: false, disableHttp3: false, headerOrder: [ 'host', 'user-agent', 'accept', 'accept-language', 'accept-encoding', 'connection', 'upgrade-insecure-requests', 'if-modified-since', 'cache-control', 'dnt', 'content-length', 'content-type', 'range', 'authorization', 'x-real-ip', 'x-forwarded-for', 'x-requested-with', 'x-csrf-token', 'x-request-id', 'sec-ch-ua', 'sec-ch-ua-mobile', 'sec-ch-ua-platform', 'sec-fetch-dest', 'sec-fetch-mode', 'sec-fetch-site', 'origin', 'referer', 'pragma', 'max-forwards', 'x-http-method-override', 'if-unmodified-since', 'if-none-match', 'if-match', 'if-range', 'accept-datetime', ], defaultHeaders: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', }, connectHeaders: null, insecureSkipVerify: false, isByteRequest: false, isByteResponse: false, isRotatingProxy: false, proxyUrl: null, defaultCookies: null, requestHostOverride: null, disableIPV6: false, disableIPV4: false, localAddress: null, serverNameOverwrite: '', streamOutputBlockSize: null, streamOutputEOFSymbol: null, streamOutputPath: null, timeoutMilliseconds: 0, timeoutSeconds: 60, withDebug: false, withDefaultCookieJar: true, withoutCookieJar: false, withRandomTLSExtensionOrder: true, retryIsEnabled: true, retryMaxCount: 3, retryStatusCodes: [408, 429, 500, 502, 503, 504, 521, 522, 523, 524], ...options, }; this.sessionId = crypto.randomUUID(); this.moduleClient = moduleClient; } private async init(): Promise<void> { if (!this.pool) { await this.moduleClient.open(); this.pool = this.moduleClient.pool; } } /** * @description Set the default cookies for the SessionClient * @param {Cookie[]} cookies - Array of cookies to set as defaults * @returns {void} */ public setDefaultCookies(cookies: Cookie[]): void { this.defaultOptions.defaultCookies = cookies; } /** * @description Set the default headers for the SessionClient * @param {Record<string, string>} headers - Object containing header key-value pairs * @returns {void} */ public setDefaultHeaders(headers: Record<string, string>): void { this.defaultOptions.defaultHeaders = headers; } private combineOptions(options: Partial<TlsClientOptions>): TlsClientOptions { // Merge default headers with request headers const headers = { ...(this.defaultOptions.defaultHeaders ?? {}), ...(options.headers ?? {}), }; // Merge default cookies with request cookies const requestCookies = [...(this.defaultOptions.defaultCookies ?? []), ...(options.requestCookies ?? [])]; // Exclude defaultHeaders and defaultCookies from being sent to Go backend const { defaultHeaders: _defaultHeaders, defaultCookies: _defaultCookies, ...baseOptions } = this.defaultOptions; return { ...baseOptions, ...options, headers, requestCookies, }; } private convertBody(body: unknown): string { if (typeof body === 'object' || Array.isArray(body)) return JSON.stringify(body); return String(body); } private convertUrl(url: unknown): string { if (!url) throw new Error('Missing url parameter'); return String(url); } /** * @description Gets the session ID if session rotation is not enabled. * @returns {string} The session ID, or null if session rotation is enabled. */ public getSession(): string { return this.sessionId; } /** * @description Destroys the sessionId * @param {string} [id=this.sessionId] - The ID associated with the memory to free. * @returns {Promise<unknown>} Promise that resolves when the session is destroyed */ public async destroySession(id: string = this.sessionId): Promise<unknown> { return this.exec('destroySession', [id]); } private async sendRequest(options: TlsClientOptions): Promise<TlsClientResponse> { return this.exec('request', [JSON.stringify(options)]) as Promise<TlsClientResponse>; } private async retryRequest(options: TlsClientOptions): Promise<TlsClientResponse> { let retryCount = 0; let response: TlsClientResponse; do { response = await this.sendRequest(options); response.retryCount = retryCount++; } while ( options.retryIsEnabled && (options.retryMaxCount ?? 0) > retryCount && (options.retryStatusCodes ?? []).includes(response.status) ); return response; } private async request(options: Partial<TlsClientOptions>): Promise<TlsClientResponse> { await this.init(); const combinedOptions = this.combineOptions(options); const request = await this.retryRequest(combinedOptions); return request; } /** * @description Send a GET request * @param {URL|string} url - The URL to send the request to * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async get(url: URL | string, options: Partial<TlsClientOptions> = {}): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'GET', requestBody: null, requestCookies: [], ...options, }); } /** * @description Send a POST request * @param {URL|string} url - The URL to send the request to * @param {object|string} body - The request body * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async post( url: URL | string, body: unknown, options: Partial<TlsClientOptions> = {} ): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'POST', requestBody: this.convertBody(body), requestCookies: [], ...options, }); } /** * @description Send a PUT request * @param {URL|string} url - The URL to send the request to * @param {object|string} body - The request body * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async put( url: URL | string, body: unknown, options: Partial<TlsClientOptions> = {} ): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'PUT', requestBody: this.convertBody(body), requestCookies: [], ...options, }); } /** * @description Send a DELETE request * @param {URL|string} url - The URL to send the request to * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async delete(url: URL | string, options: Partial<TlsClientOptions> = {}): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'DELETE', requestBody: '', requestCookies: [], ...options, }); } /** * @description Send a HEAD request * @param {URL|string} url - The URL to send the request to * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async head(url: URL | string, options: Partial<TlsClientOptions> = {}): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'HEAD', requestBody: '', requestCookies: [], ...options, }); } /** * @description Send a PATCH request * @param {URL|string} url - The URL to send the request to * @param {object|string} body - The request body * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async patch( url: URL | string, body: unknown, options: Partial<TlsClientOptions> = {} ): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'PATCH', requestBody: this.convertBody(body), requestCookies: [], ...options, }); } /** * @description Send an OPTIONS request * @param {URL|string} url - The URL to send the request to * @param {Partial<TlsClientOptions>} [options={}] - The request options * @returns {Promise<TlsClientResponse>} The response from the server */ public async options(url: URL | string, options: Partial<TlsClientOptions> = {}): Promise<TlsClientResponse> { return this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: 'OPTIONS', requestBody: '', requestCookies: [], ...options, }); } /** * @description Get the cookies for a given session and URL * @param {string} sessionId - The existing session ID. * @param {string} url - The URL to get cookies for. * @returns {Promise<CookieResponse>} Promise that resolves to the cookie response */ public async getCookiesFromSession(sessionId: string, url: string): Promise<CookieResponse> { if (!sessionId || !url) throw new Error('Missing sessionId or url parameter'); await this.init(); const response = this.exec('getCookiesFromSession', [JSON.stringify({ sessionId, url })]); return response as Promise<CookieResponse>; } /** * @deprecated Use requestCookies instead * @description Add cookies to a given session * @param {string} sessionId - The existing session ID. * @param {string} url - The URL to add cookies for. * @param {Cookie[]} cookies - The cookies to add. * @returns {Promise<CookieResponse>} Promise that resolves to the cookie response */ public async addCookiesToSession(sessionId: string, url: string, cookies: Cookie[]): Promise<CookieResponse> { if (!sessionId || !url || !cookies) throw new Error('Missing sessionId, url or cookies parameter'); await this.init(); const response = this.exec('addCookiesToSession', [JSON.stringify({ sessionId, url, cookies })]); return response as Promise<CookieResponse>; } /** * @description Destroy all existing sessions in order to release allocated memory. * @returns {Promise<unknown>} Promise that resolves when all sessions are destroyed */ public destroyAll(): Promise<unknown> { return this.exec('destroyAll', []); } // Method to exec and then run freeMemory private async exec(func: string, args: unknown[]): Promise<unknown> { await this.init(); if (!this.pool) { throw new Error('Worker pool not initialized'); } const result = await this.pool.run({ fn: func, args, }); return result; } } export default { SessionClient, ModuleClient }; export { ModuleClient }; export { type ModuleClientOptions, type PoolStats } from './utils/client.js';