UNPKG

tlsclientwrapper

Version:

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

580 lines (574 loc) 18.9 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.23.1_typescript@5.9.3/node_modules/tsup/assets/esm_shims.js import path from "path"; import { fileURLToPath } from "url"; var getFilename = /* @__PURE__ */ __name(() => fileURLToPath(import.meta.url), "getFilename"); var getDirname = /* @__PURE__ */ __name(() => path.dirname(getFilename()), "getDirname"); var __dirname = /* @__PURE__ */ getDirname(); var __filename = /* @__PURE__ */ getFilename(); // src/utils/client.ts import Piscina from "piscina"; // src/utils/path.ts import fs from "fs"; import os from "os"; import path2 from "path"; var TlsDependency = class { static { __name(this, "TlsDependency"); } arch; platform; version; filename; extension; distribution; constructor() { this.arch = os.arch(); this.platform = os.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 ?? os.tmpdir(); if (!fs.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: path2.join(downloadFolder, filename) }; } }; var path_default = TlsDependency; // src/utils/client.ts import path3 from "path"; import fs2 from "fs"; import { rename, writeFile } from "fs/promises"; import os2 from "os"; import { isMainThread } from "worker_threads"; function getWorkerPath() { const ext = typeof __dirname !== "undefined" && __filename.endsWith(".cjs") ? "cjs" : "mjs"; return path3.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(os2.cpus().length, 1) * 2; } /** * @description Checks if the TLS library exists. * @returns {boolean} True if the library exists, false otherwise. */ libraryExists() { return fs2.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 writeFile(tempPath, Buffer.from(await response.arrayBuffer())); await 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 (isMainThread) { await this.downloadLibrary(); } this.pool = this.startWorkerPool(); } /** * @description Starts the worker pool. * @returns {Piscina} The Piscina worker pool. */ startWorkerPool() { return new Piscina({ 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 import crypto from "crypto"; 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 = crypto.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 }; export { client_default as ModuleClient, SessionClient, index_default as default };