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
JavaScript
"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
});