UNPKG

tlsclientwrapper

Version:

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

608 lines (602 loc) 20.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ModuleClient: () => client_default, SessionClient: () => SessionClient, default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/utils/client.ts var import_piscina = __toESM(require("piscina"), 1); // src/utils/path.ts var import_node_fs = __toESM(require("fs"), 1); var import_node_os = __toESM(require("os"), 1); var import_node_path = __toESM(require("path"), 1); var TlsDependency = class { static { __name(this, "TlsDependency"); } arch; platform; version; filename; extension; distribution; constructor() { this.arch = import_node_os.default.arch(); this.platform = import_node_os.default.platform(); this.version = "1.15.1"; this.filename = "tls-client-xgo"; this.extension = ""; this.distribution = ""; this.setDetails(); } setDetails() { if (this.platform === "win32") { this.extension = "dll"; this.distribution = this.arch.includes("64") ? "windows-amd64" : "windows-386"; } else if (this.platform === "darwin") { this.extension = "dylib"; this.distribution = this.arch === "arm64" ? "darwin-arm64" : "darwin-amd64"; } else if (this.platform === "linux") { this.extension = "so"; const archMap = { arm64: "linux-arm64", x64: "linux-amd64", ia32: "linux-386", arm: "linux-arm-7", // assuming ARMv7 ppc64: "linux-ppc64le", riscv64: "linux-riscv64", s390x: "linux-s390x" }; const distribution = archMap[this.arch]; if (!distribution) { console.error(`Unsupported architecture: ${this.arch}, defaulting to linux-amd64`); } this.distribution = distribution ?? "linux-amd64"; } else { throw new Error(`Unsupported platform: ${this.platform}`); } } getTLSDependencyPath(customPath) { const filename = `${this.filename}-${this.version}-${this.distribution}.${this.extension}`; const downloadFolder = customPath ?? import_node_os.default.tmpdir(); if (!import_node_fs.default.existsSync(downloadFolder)) { throw new Error(`The download folder does not exist: ${downloadFolder}`); } return { DOWNLOAD_PATH: `https://github.com/bogdanfinn/tls-client/releases/download/v${this.version}/${filename}`, TLS_LIB_PATH: import_node_path.default.join(downloadFolder, filename) }; } }; var path_default = TlsDependency; // src/utils/client.ts var import_node_path2 = __toESM(require("path"), 1); var import_node_fs2 = __toESM(require("fs"), 1); var import_promises = require("fs/promises"); var import_node_os2 = __toESM(require("os"), 1); var import_node_worker_threads = require("worker_threads"); function getWorkerPath() { const ext = typeof __dirname !== "undefined" && __filename.endsWith(".cjs") ? "cjs" : "mjs"; return import_node_path2.default.resolve(__dirname, "utils", `worker.${ext}`); } __name(getWorkerPath, "getWorkerPath"); var ModuleClient = class { static { __name(this, "ModuleClient"); } customPath; tlsDependency; tlsDependencyPath; TLS_LIB_PATH; maxThreads; pool = null; opening = null; /** * @description Creates a new ModuleClient instance. * @param {ModuleClientOptions} [options] - Configuration options for the ModuleClient * @example const module = new ModuleClient(); * @example const module = new ModuleClient({ customLibraryPath: '/path/to/tls-library' }); */ constructor(options) { this.customPath = options?.customLibraryPath ? true : false; this.tlsDependency = new path_default(); this.tlsDependencyPath = this.tlsDependency.getTLSDependencyPath(options?.customLibraryDownloadPath); const libPath = this.customPath ? options?.customLibraryPath : this.tlsDependencyPath?.TLS_LIB_PATH; if (!libPath) { throw new Error("TLS library path not available"); } this.TLS_LIB_PATH = libPath; this.maxThreads = options?.maxThreads ?? Math.max(import_node_os2.default.cpus().length, 1) * 2; } /** * @description Checks if the TLS library exists. * @returns {boolean} True if the library exists, false otherwise. */ libraryExists() { return import_node_fs2.default.existsSync(this.TLS_LIB_PATH); } /** * @description Downloads the TLS library if it does not exist. * @returns {Promise<void>} Promise that resolves when the library is downloaded */ async downloadLibrary() { if (this.libraryExists()) return; if (this.customPath) { throw new Error("Custom path provided but library does not exist: " + this.TLS_LIB_PATH); } const downloadPath = this.tlsDependencyPath?.DOWNLOAD_PATH; if (!downloadPath) { throw new Error("Download path not available"); } console.log("[tlsClient] Detected missing TLS library"); console.log("[tlsClient] DownloadPath: " + downloadPath); console.log("[tlsClient] DestinationPath: " + this.TLS_LIB_PATH); console.log("[tlsClient] Downloading TLS library... This may take a while"); const response = await fetch(downloadPath); if (!response.ok) { throw new Error(`Unexpected response ${response.statusText}`); } const tempPath = `${this.TLS_LIB_PATH}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`; await (0, import_promises.writeFile)(tempPath, Buffer.from(await response.arrayBuffer())); await (0, import_promises.rename)(tempPath, this.TLS_LIB_PATH); console.log("[tlsClient] Successfully downloaded TLS library"); } /** * @description Opens the TLS library and initializes the worker pool. * @returns {Promise<void>} Promise that resolves when the library is opened and pool is initialized */ async open() { this.opening ??= this.initialize(); try { await this.opening; } catch (error) { this.opening = null; throw error; } } async initialize() { if (import_node_worker_threads.isMainThread) { await this.downloadLibrary(); } this.pool = this.startWorkerPool(); } /** * @description Starts the worker pool. * @returns {Piscina} The Piscina worker pool. */ startWorkerPool() { return new import_piscina.default({ filename: getWorkerPath(), workerData: { libraryPath: this.TLS_LIB_PATH }, atomics: "disabled", idleTimeout: 3e4, minThreads: 1, maxThreads: this.maxThreads }); } /** * @description Get current pool statistics. * @returns {PoolStats | null} Pool statistics. */ getPoolStats() { if (!this.pool) return null; return { utilization: this.pool.utilization, completed: this.pool.completed, waiting: this.pool.queueSize, threads: this.pool.threads.length }; } /** * @description Terminates the worker pool and unloads the TLS library. * @returns {Promise<boolean>} True if the termination was successful, false otherwise. */ async terminate() { if (this.opening) { await this.opening.catch(() => void 0); } const pool = this.pool; this.pool = null; this.opening = null; if (!pool) return true; try { await pool.run({ fn: "destroyAll", args: [] }); await pool.destroy(); return true; } catch (error) { console.error("Error during ModuleClient termination:", error); return false; } } }; var client_default = ModuleClient; // src/index.ts var import_node_crypto = __toESM(require("crypto"), 1); var sessionFinalizationRegistry = new FinalizationRegistry( (held) => { if (!held.moduleClient.pool) return; void held.moduleClient.pool.run({ fn: "destroySession", args: [JSON.stringify({ sessionId: held.sessionId })] }).catch(() => { }); } ); var DEFAULT_OPTIONS = { tlsClientIdentifier: "chrome_146", catchPanics: false, certificatePinningHosts: null, customTlsClient: null, customLibraryDownloadPath: null, transportOptions: null, followRedirects: false, forceHttp1: false, disableHttp3: false, withProtocolRacing: 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/146.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, withCustomCookieJar: false, withoutCookieJar: false, withRandomTLSExtensionOrder: true, retryIsEnabled: true, retryMaxCount: 3, retryStatusCodes: [408, 429, 500, 502, 503, 504, 521, 522, 523, 524] }; var SessionClient = class { static { __name(this, "SessionClient"); } defaultOptions; sessionId; moduleClient; destroyed = false; /** * @description Create a new SessionClient * @param {ModuleClient} moduleClient - The shared ModuleClient instance * @param {TlsClientDefaultOptions} [options={}] - SessionClient options */ constructor(moduleClient, options = {}) { 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 client_default)) { throw new Error("ModuleClient must be an instance of ModuleClient"); } this.defaultOptions = { ...DEFAULT_OPTIONS, headerOrder: [...DEFAULT_OPTIONS.headerOrder ?? []], defaultHeaders: { ...DEFAULT_OPTIONS.defaultHeaders }, retryStatusCodes: [...DEFAULT_OPTIONS.retryStatusCodes ?? []], ...options }; this.sessionId = import_node_crypto.default.randomUUID(); this.moduleClient = moduleClient; sessionFinalizationRegistry.register( this, { sessionId: this.sessionId, moduleClient: this.moduleClient }, this ); } /** * Explicit async cleanup for `await using session = new SessionClient(...)` (TypeScript 5.2+). */ async [Symbol.asyncDispose]() { await this.destroySession(); } /** * @description Set the default cookies for the SessionClient * @param {Cookie[]} cookies - Array of cookies to set as defaults * @returns {void} */ setDefaultCookies(cookies) { 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} */ setDefaultHeaders(headers) { this.defaultOptions.defaultHeaders = headers; } combineOptions(options) { const headers = { ...this.defaultOptions.defaultHeaders, ...options.headers }; const requestCookies = [...this.defaultOptions.defaultCookies ?? [], ...options.requestCookies ?? []]; const { defaultHeaders: _defaultHeaders, defaultCookies: _defaultCookies, ...baseOptions } = this.defaultOptions; return { ...baseOptions, ...options, headers, requestCookies }; } convertBody(body) { if (body === null || body === void 0) return null; if (typeof body === "string") return body; if (typeof body === "object") return JSON.stringify(body); return String(body); } convertUrl(url) { return url instanceof URL ? url.toString() : url; } /** * @description Gets the session ID. * @returns {string} The session ID. */ getSession() { 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 */ async destroySession(id = this.sessionId) { const isOwn = id === this.sessionId; if (isOwn && this.destroyed) { return void 0; } if (isOwn) sessionFinalizationRegistry.unregister(this); try { const result = await this.exec("destroySession", [JSON.stringify({ sessionId: id })]); if (isOwn) this.destroyed = true; return result; } catch (error) { if (isOwn) { sessionFinalizationRegistry.register( this, { sessionId: this.sessionId, moduleClient: this.moduleClient }, this ); } throw error; } } async sendRequest(options) { const { retryIsEnabled: _retryIsEnabled, retryMaxCount: _retryMaxCount, retryStatusCodes: _retryStatusCodes, customLibraryDownloadPath: _customLibraryDownloadPath, euckrResponse: _euckrResponse, ...goOptions } = options; const response = await this.exec("request", [JSON.stringify(goOptions)]); response.headers ??= {}; response.cookies ??= {}; return response; } async retryRequest(options) { let retryCount = 0; let response; do { response = await this.sendRequest(options); response.retryCount = retryCount++; } while (options.retryIsEnabled && (options.retryMaxCount ?? 0) > retryCount && (options.retryStatusCodes ?? []).includes(response.status)); return response; } async request(options) { return await this.retryRequest(this.combineOptions(options)); } async send(method, url, body, options) { return await this.request({ sessionId: this.sessionId, requestUrl: this.convertUrl(url), requestMethod: method, requestBody: this.convertBody(body), requestCookies: [], ...options }); } /** * @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 */ async get(url, options = {}) { return await this.send("GET", url, null, 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 */ async post(url, body = null, options = {}) { return await this.send("POST", url, body, 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 */ async put(url, body = null, options = {}) { return await this.send("PUT", url, body, 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 */ async delete(url, options = {}) { return await this.send("DELETE", url, null, 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 */ async head(url, options = {}) { return await this.send("HEAD", url, null, 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 */ async patch(url, body = null, options = {}) { return await this.send("PATCH", url, body, 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 */ async options(url, options = {}) { return await this.send("OPTIONS", url, null, 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 */ async getCookiesFromSession(sessionId, url) { if (!sessionId || !url) throw new Error("Missing sessionId or url parameter"); return await this.exec("getCookiesFromSession", [JSON.stringify({ sessionId, url })]); } /** * @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 */ async addCookiesToSession(sessionId, url, cookies) { if (!sessionId || !url || !cookies) throw new Error("Missing sessionId, url or cookies parameter"); return await this.exec("addCookiesToSession", [ JSON.stringify({ sessionId, url, cookies }) ]); } /** * @description Destroy all existing sessions in order to release allocated memory. * @returns {Promise<unknown>} Promise that resolves when all sessions are destroyed */ destroyAll() { return this.exec("destroyAll", []); } async exec(func, args) { if (this.destroyed && func !== "destroySession") { throw new Error("SessionClient has been destroyed"); } await this.moduleClient.open(); const pool = this.moduleClient.pool; if (!pool) { throw new Error("Worker pool not initialized"); } return await pool.run({ fn: func, args }); } }; var index_default = { SessionClient, ModuleClient: client_default }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ModuleClient, SessionClient });