UNPKG

terabox-api

Version:

NodeJS tool for interacting with the TeraBox cloud service without the need to use the website or app ☁️

1,335 lines (1,206 loc) 93.2 kB
import { FormData, Client, buildConnector, request } from 'undici'; import { Cookie, CookieJar } from 'tough-cookie'; import { filesize } from 'filesize'; import child_process from 'node:child_process'; import crypto from 'node:crypto'; import tls from 'node:tls'; /** * Constructs a remote file path by combining a directory and filename, ensuring proper slash formatting * @param {string} sdir - The directory path (with or without trailing slash) * @param {string} sfile - The filename to append to the directory path * @returns {string} The combined full path with exactly one slash between directory and filename * @example * makeRemoteFPath('documents', 'file.txt') // returns 'documents/file.txt' * makeRemoteFPath('documents/', 'file.txt') // returns 'documents/file.txt' */ function makeRemoteFPath(sdir, sfile){ const tdir = sdir.match(/\/$/) ? sdir : sdir + '/'; return tdir + sfile; } /** * A utility class for handling application/x-www-form-urlencoded data * Wraps URLSearchParams with additional convenience methods and encoding behavior */ class FormUrlEncoded { /** * Creates a new FormUrlEncoded instance * @param {Object.<string, string>} [params] - Optional initial parameters as key-value pairs * @example * const form = new FormUrlEncoded({ foo: 'bar', baz: 'qux' }); */ constructor(params) { /** * @private * @type {URLSearchParams} */ this.data = new URLSearchParams(); if(typeof params === 'object' && params !== null){ for (const [key, value] of Object.entries(params)) { this.data.append(key, value); } } } /** * Sets or replaces a parameter value * @param {string} param - The parameter name * @param {string} value - The parameter value * @returns {void} */ set(param, value){ this.data.set(param, value); } /** * Appends a new value to an existing parameter * @param {string} param - The parameter name * @param {string} value - The parameter value * @returns {void} */ append(param, value){ this.data.append(param, value); } /** * Removes a parameter * @param {string} param - The parameter name to remove * @returns {void} */ delete(param){ this.data.delete(param); } /** * Returns the encoded string representation (space encoded as %20) * Suitable for application/x-www-form-urlencoded content * @returns {string} The encoded form data * @example * form.str(); // returns "foo=bar&baz=qux" */ str(){ return this.data.toString().replace(/\+/g, '%20'); } /** * Returns the underlying URLSearchParams object * @returns {URLSearchParams} The native URLSearchParams instance */ url(){ return this.data; } } /** * Generates a signed download token using a modified RC4-like algorithm * * This function implements a stream cipher similar to RC4 that: * 1. Initializes a permutation array using the secret key (s1) * 2. Generates a pseudorandom keystream * 3. XORs the input data (s2) with the keystream * 4. Returns the result as a Base64-encoded string * * @param {string} s1 - The secret key used for signing (should be at least 1 character) * @param {string} s2 - The input data to be signed * @returns {string} Base64-encoded signature * @example * const signature = signDownload('secret-key', 'data-to-sign'); * // Returns something like: "X3p8YFJjUA==" */ function signDownload(s1, s2) { // Initialize permutation array (p) and key array (a) const p = new Uint8Array(256); const a = new Uint8Array(256); const result = []; // Key-scheduling algorithm (KSA) // Initialize the permutation array with the secret key Array.from({ length: 256 }, (_, i) => { a[i] = s1.charCodeAt(i % s1.length); p[i] = i; }); // Scramble the permutation array using the key let j = 0; Array.from({ length: 256 }, (_, i) => { j = (j + p[i] + a[i]) % 256; [p[i], p[j]] = [p[j], p[i]]; // swap }); // Pseudo-random generation algorithm (PRGA) // Generate keystream and XOR with input data let i = 0; j = 0; Array.from({ length: s2.length }, (_, q) => { i = (i + 1) % 256; j = (j + p[i]) % 256; [p[i], p[j]] = [p[j], p[i]]; // swap const k = p[(p[i] + p[j]) % 256]; result.push(s2.charCodeAt(q) ^ k); }); // Return the result as Base64 return Buffer.from(result).toString('base64'); } /** * Validates whether a string is a properly formatted MD5 hash * * Checks if the input: * 1. Is exactly 32 characters long * 2. Contains only hexadecimal characters (a-f, 0-9) * 3. Is in lowercase * * Note: This only validates the format, not the cryptographic correctness of the hash. * * @param {*} md5 - The value to check (typically a string) * @returns {boolean} True if the input is a valid MD5 format, false otherwise * @example * checkMd5val('d41d8cd98f00b204e9800998ecf8427e') // returns true * checkMd5val('D41D8CD98F00B204E9800998ECF8427E') // returns false (uppercase) * checkMd5val('z41d8cd98f00b204e9800998ecf8427e') // returns false (invalid character) * checkMd5val('d41d8cd98f') // returns false (too short) */ function checkMd5val(md5){ if(typeof md5 !== 'string') return false; return /^[a-f0-9]{32}$/.test(md5); } /** * Validates that all elements in an array are properly formatted MD5 hashes * * Checks if: * 1. The input is an array * 2. Every element in the array passes checkMd5val() validation * (32-character hexadecimal strings in lowercase) * * @param {*} arr - The array to validate * @returns {boolean} True if all elements are valid MD5 hashes, false otherwise * (also returns false if input is not an array) * @see checkMd5val For individual MD5 hash validation logic * * @example * checkMd5arr(['d41d8cd98f00b204e9800998ecf8427e', '5d41402abc4b2a76b9719d911017c592']) // true * checkMd5arr(['d41d8cd98f00b204e9800998ecf8427e', 'invalid']) // false * checkMd5arr('not an array') // false * checkMd5arr([]) // false (empty array is considered invalid) */ function checkMd5arr(arr) { if (!Array.isArray(arr)) return false; if (arr.length === 0) return false; return arr.every(item => { return checkMd5val(item); }); } /** * Applies a custom transformation to what appears to be an MD5 hash * * This function performs a series of reversible transformations on an input string * that appears to be an MD5 hash (32 hexadecimal characters). The transformation includes: * 1. Character restoration at position 9 * 2. XOR operation with position-dependent values * 3. Byte reordering of the result * * @param {string} md5 - The input string (expected to be 32 hexadecimal characters) * @returns {string} The transformed result (32 hexadecimal characters) * @throws Will return the original input unchanged if length is not 32 * * @example * decodeMd5('a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6') // returns transformed value * decodeMd5('short') // returns 'short' (unchanged) */ function decodeMd5(md5) { // Return unchanged if not 32 characters if (md5.length !== 32) return md5; // Restore character at position 9 const restoredHexChar = (md5.charCodeAt(9) - 'g'.charCodeAt(0)).toString(16); const o = md5.slice(0, 9) + restoredHexChar + md5.slice(10); // Apply XOR transformation to each character let n = ''; for (let i = 0; i < o.length; i++) { const orig = parseInt(o[i], 16) ^ (i & 15); n += orig.toString(16); } // Reorder the bytes in the result const e = n.slice(8, 16) + // original bytes 8-15 (now first) n.slice(0, 8) + // original bytes 0-7 (now second) n.slice(24, 32) + // original bytes 24-31 (now third) n.slice(16, 24); // original bytes 16-23 (now last) return e; } /** * Converts between standard and URL-safe Base64 encoding formats * * Base64 strings may contain '+', '/' and '=' characters that need to be replaced * for safe use in URLs. This function provides bidirectional conversion: * - Mode 1: Converts to URL-safe Base64 (RFC 4648 §5) * - Mode 2: Converts back to standard Base64 * * @param {string} str - The Base64 string to convert * @param {number} [mode=1] - Conversion direction: * 1 = to URL-safe (default), * 2 = to standard * @returns {string} The converted Base64 string * * @example * // To URL-safe Base64 * changeBase64Type('a+b/c=') // returns 'a-b_c=' * * // To standard Base64 * changeBase64Type('a-b_c=', 2) // returns 'a+b/c=' * * @see {@link https://tools.ietf.org/html/rfc4648#section-5|RFC 4648 §5} for URL-safe Base64 */ function changeBase64Type(str, mode = 1) { return mode === 1 ? str.replace(/\+/g, '-').replace(/\//g, '_') // to url-safe : str.replace(/-/g, '+').replace(/_/g, '/'); // to standard } /** * Decrypts AES-128-CBC encrypted data using provided parameters * * This function: * 1. Converts both parameters from URL-safe Base64 to standard Base64 * 2. Extracts the IV (first 16 bytes) and ciphertext from pp1 * 3. Uses pp2 as the decryption key * 4. Performs AES-128-CBC decryption * * @param {string} pp1 - Combined IV and ciphertext in URL-safe Base64 format: * First 16 bytes are IV, remainder is ciphertext * @param {string} pp2 - Encryption key in URL-safe Base64 format * @returns {string} The decrypted UTF-8 string * @throws {Error} May throw errors for invalid inputs or decryption failures * * @example * // Example usage (with actual encrypted data) * const decrypted = decryptAES( * 'MTIzNDU2Nzg5MDEyMzQ1Ng==...', // IV + ciphertext * 'c2VjcmV0LWtleS1kYXRhCg==' // Key * ); * * @requires crypto Node.js crypto module * @see changeBase64Type For Base64 format conversion */ function decryptAES(pp1, pp2) { // Convert from URL-safe Base64 to standard Base64 pp1 = changeBase64Type(pp1, 2); pp2 = changeBase64Type(pp2, 2); // Extract ciphertext (after first 16 bytes) and IV (first 16 bytes) const cipherText = pp1.substring(16); const key = Buffer.from(pp2, 'utf8'); const iv = Buffer.from(pp1.substring(0, 16), 'utf8'); // Create decipher with AES-128-CBC algorithm const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv); // Perform decryption let decrypted = decipher.update(cipherText, 'base64', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } /** * Encrypts data using RSA with a public key, with optional MD5 preprocessing * * Supports two encryption modes: * 1. Direct encryption of the message (default) * 2. MD5 hash preprocessing (applies MD5 + length padding before encryption) * * @param {string} message - The plaintext message to encrypt * @param {string|Buffer} publicKeyPEM - RSA public key in PEM format * @param {number} [mode=1] - Encryption mode: * 1 = direct encryption, * 2 = MD5 hash preprocessing * @returns {string} Base64-encoded encrypted data * @throws {Error} May throw errors for invalid keys or encryption failures * * @example * // Direct encryption * encryptRSA('secret message', publicKey); * * // With MD5 preprocessing * encryptRSA('secret message', publicKey, 2); * * @requires crypto Node.js crypto module */ function encryptRSA(message, publicKeyPEM, mode = 1) { // Mode 2: Apply MD5 hash and length padding if (mode === 2) { const md5 = crypto.createHash('md5').update(message).digest('hex'); message = md5 + (md5.length<10?'0':'') + md5.length; } // Convert message to Buffer const buffer = Buffer.from(message, 'utf8'); // Perform RSA encryption const encrypted = crypto.publicEncrypt({ key: publicKeyPEM, padding: crypto.constants.RSA_PKCS1_PADDING, }, buffer, ); // Return as Base64 string return encrypted.toString('base64'); } /** * Generates a pseudo-random SHA-1 hash from combined client parameters * * Creates a deterministic hash value by combining multiple client-specific parameters. * This is typically used for generating session tokens or unique identifiers. * * @param {string} [client='web'] - Client identifier (e.g., 'web', 'mobile') * @param {string} seval - Session evaluation parameter * @param {string} encpwd - Encrypted password or password hash * @param {string} email - User's email address * @param {string} [browserid=''] - Browser fingerprint or identifier * @param {string} random - Random value * @returns {string} SHA-1 hash of the combined parameters (40-character hex string) * * @example * // Basic usage * const token = prandGen('web', 'session123', 'encryptedPwd', 'user@example.com', 'browser123', 'randomValue'); * * // With default client and empty browserid * const token = prandGen(undefined, 'session123', 'encryptedPwd', 'user@example.com', '', 'randomValue'); * * @requires crypto Node.js crypto module */ function prandGen(client = 'web', seval, encpwd, email, browserid = '', random) { // Combine all parameters with hyphens const combined = `${client}-${seval}-${encpwd}-${email}-${browserid}-${random}`; // Generate SHA-1 hash and return as hex string return crypto.createHash('sha1').update(combined).digest('hex'); } /** * TeraBox API Client Class * * Provides a comprehensive interface for interacting with TeraBox services, * including encryption utilities, API request handling, and session management. * * @class * @exports TeraBoxApp * @property {object} FormUrlEncoded - Form URL encoding utility * @property {function} SignDownload - Download signature generator * @property {function} CheckMd5Val - MD5 hash validator (single) * @property {function} CheckMd5Arr - MD5 hash validator (array) * @property {function} DecodeMd5 - Custom MD5 transformation * @property {function} ChangeBase64Type - Base64 format converter * @property {function} DecryptAES - AES decryption utility * @property {function} EncryptRSA - RSA encryption utility * @property {function} PRandGen - Pseudo-random hash generator * @property {string} TERABOX_DOMAIN - Default Terabox domain * @property {number} TERABOX_TIMEOUT - Default API timeout (10 seconds) */ class TeraBoxApp { // Encryption/Utility Methods 1 FormUrlEncoded = FormUrlEncoded; SignDownload = signDownload; CheckMd5Val = checkMd5val; CheckMd5Arr = checkMd5arr; DecodeMd5 = decodeMd5; // Encryption/Utility Methods 2 ChangeBase64Type = changeBase64Type; DecryptAES = decryptAES; EncryptRSA = encryptRSA; PRandGen = prandGen; /** * Default Terabox Domain * @type {string} */ TERABOX_DOMAIN = 'terabox.com'; /** * Default API timeout in milliseconds (10 seconds) * @type {number} */ TERABOX_TIMEOUT = 10000; /** * Application data including tokens and keys * @type {Object} * @property {string} csrf - CSRF token * @property {string} logid - Log ID * @property {string} pcftoken - PCF token * @property {string} bdstoken - BDS token * @property {string} jsToken - JavaScript token * @property {string} pubkey - Public key */ data = { csrf: '', logid: '0', pcftoken: '', bdstoken: '', jsToken: '', pubkey: '', }; /** * Application parameters and configuration * @type {Object} * @property {string} bhost - base host name * @property {string} whost - Web host URL * @property {string} uhost - Upload host URL * @property {string} lang - Language setting * @property {Object} app - Application settings * @property {number} app.app_id - Application ID * @property {number} app.web - Web flag * @property {string} app.channel - Channel identifier * @property {number} app.clienttype - Client type * @property {string} ver_android - Android version * @property {string} ua - User agent string * @property {string} cookie - Cookie string * @property {Object} auth - Authentication data * @property {number} account_id - Account ID * @property {string} account_name - Account name * @property {boolean} is_vip - VIP status flag * @property {number} vip_type - VIP type * @property {number} space_used - Used space in bytes * @property {number} space_total - Total space in bytes * @property {number} space_available - Available space in bytes * @property {string} cursor - Cursor for pagination */ params = { whost: 'https://www.' + this.TERABOX_DOMAIN, uhost: 'https://c-www.' + this.TERABOX_DOMAIN, lang: 'en', app: { app_id: 250528, web: 1, channel: 'dubox', clienttype: 0, // 5 is wap? }, ver_android: '3.44.2', ua: 'terabox;1.40.0.132;PC;PC-Windows;10.0.26100;WindowsTeraBox', cookie: '', auth: {}, account_id: 0, account_name: '', is_vip: false, vip_type: 0, space_used: 0, space_total: Math.pow(1024, 3), space_available: Math.pow(1024, 3), cursor: 'null', }; /** * Creates a new TeraBoxApp instance * @param {string} authData - Authentication data (NDUS token) * @param {string} [authType='ndus'] - Authentication type (currently only 'ndus' supported) * @throws {Error} Throws error if authType is not supported */ constructor(authData, authType = 'ndus') { this.params.cookie = `lang=${this.params.lang}`; if(authType === 'ndus'){ this.params.cookie += authData ? '; ndus=' + authData : ''; } else{ throw new Error('initTBApp', { cause: 'AuthType Not Supported!' }); } } /** * Updates application data including tokens and user information * @param {string} [customPath] - Custom path to use for the update request * @param {number} [retries=4] - Number of retry attempts * @returns {Promise<Object>} The updated template data * @async * @throws {Error} Throws error if request fails or parsing fails */ async updateAppData(customPath, retries = 4){ const url = new URL(this.params.whost + (customPath ? `/${customPath}` : '/main')); try{ const req = await request(url, { headers:{ 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT + 10000), }); if(req.statusCode === 302){ const newUrl = new URL(req.headers.location); if(this.params.whost !== newUrl.origin){ this.params.whost = newUrl.origin; console.warn(`[WARN] Default hostname changed to ${newUrl.origin}`); } const toPathname = newUrl.pathname.replace(/^\//, ''); const finalUrl = toPathname + newUrl.search; return await this.updateAppData(finalUrl, retries); } if(req.headers['set-cookie']){ const cJar = new CookieJar(); this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost)); if(typeof req.headers['set-cookie'] === 'string'){ req.headers['set-cookie'] = [req.headers['set-cookie']]; } for(const cookie of req.headers['set-cookie']){ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost); } this.params.cookie = cJar.getCookiesSync(this.params.whost).map(cookie => cookie.cookieString()).join('; '); } const rdata = await req.body.text(); const tdataRegex = /<script>var templateData = (.*);<\/script>/; const jsTokenRegex = /window.jsToken%20%3D%20a%7D%3Bfn%28%22(.*)%22%29/; const tdata = rdata.match(tdataRegex) ? JSON.parse(rdata.match(tdataRegex)[1].split(';</script>')[0]) : {}; const isLoginReq = req.headers.location === '/login' ? true : false; if(tdata.jsToken){ tdata.jsToken = tdata.jsToken.match(/%28%22(.*)%22%29/)[1]; } else if(rdata.match(jsTokenRegex)){ tdata.jsToken = rdata.match(jsTokenRegex)[1]; } else if(isLoginReq){ console.error('[ERROR] Failed to update jsToken [Login Required]'); } if(req.headers.logid){ this.data.logid = req.headers.logid; } this.data.csrf = tdata.csrf || ''; this.data.pcftoken = tdata.pcftoken || ''; this.data.bdstoken = tdata.bdstoken || ''; this.data.jsToken = tdata.jsToken || ''; this.params.account_id = parseInt(tdata.uk) || 0; if(typeof tdata.userVipIdentity === 'number' && tdata.userVipIdentity > 0){ this.params.is_vip = true; //this.params.vip_type = 2; } return tdata; } catch(error){ if(error.name === 'TimeoutError' && retries > 0){ await new Promise(resolve => setTimeout(resolve, 500)); return await this.updateAppData(customPath, retries - 1); } const errorPrefix = '[ERROR] Failed to update jsToken:'; if(error.name === 'TimeoutError'){ console.error(errorPrefix, error.message); return; } error = new Error('updateAppData', { cause: error }); console.error(errorPrefix, error); } } /** * Sets default VIP parameters * @returns {void} */ setVipDefaults(){ this.params.is_vip = true; this.params.vip_type = 2; this.params.space_total = Math.pow(1024, 3) * 2; this.params.space_available = Math.pow(1024, 3) * 2; } /** * Makes an API request with retry logic * @param {string} req_url - The request URL (relative to whost) * @param {Object} [req_options={}] - Request options (headers, body, etc.) * @param {number} [retries=4] - Number of retry attempts * @returns {Promise<Object>} The JSON-parsed response data * @async * @throws {Error} Throws error if all retries fail */ async doReq(req_url, req_options = {}, retries = 4){ const url = new URL(this.params.whost + req_url); let reqm_options = structuredClone(req_options); let req_headers = {}; if(reqm_options.headers){ req_headers = reqm_options.headers; delete reqm_options.headers; } const save_cookies = reqm_options.save_cookies; delete reqm_options.save_cookies; const silent_retry = reqm_options.silent_retry; delete reqm_options.silent_retry; const req_timeout = reqm_options.timeout ? reqm_options.timeout : this.TERABOX_TIMEOUT; delete reqm_options.timeout; try { const options = { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, ...req_headers, }, ...reqm_options, signal: AbortSignal.timeout(req_timeout), }; const req = await request(url, options); if(save_cookies && req.headers['set-cookie']){ const cJar = new CookieJar(); this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost)); if(typeof req.headers['set-cookie'] === 'string'){ req.headers['set-cookie'] = [req.headers['set-cookie']]; } for(const cookie of req.headers['set-cookie']){ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost); } this.params.cookie = cJar.getCookiesSync(this.params.whost).map(cookie => cookie.cookieString()).join('; '); } const rdata = await req.body.json(); return rdata; } catch(error){ if (retries > 0) { await new Promise(resolve => setTimeout(resolve, 500)); if(!silent_retry){ console.error('[ERROR] DoReq:', req_url, '|', error.code, ':', error.message, '(retrying...)'); } return await this.doReq(req_url, req_options, retries - 1); } throw new Error('doReq', { cause: error }); } } /** * Retrieves system configuration from the TeraBox API * @returns {Promise<Object>} The system configuration JSON data * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async getSysCfg(){ const url = new URL(this.params.whost + '/api/getsyscfg'); url.search = new URLSearchParams({ clienttype: this.params.app.clienttype, language_type: this.params.lang, cfg_category_keys: '[]', version: 0, }); try{ const req = await request(url, { headers: { 'User-Agent': this.params.ua, // 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); return rdata; } catch(error){ throw new Error('getSysCfg', { cause: error }); } } /** * Checks login status of the current session * @returns {Promise<Object>} The login status JSON data * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async checkLogin(){ const url = new URL(this.params.whost + '/api/check/login'); try{ const req = await request(url, { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const regionPrefix = req.headers['region-domain-prefix']; if(regionPrefix){ const newHostname = `https://${regionPrefix}.${this.TERABOX_DOMAIN}`; console.warn(`[WARN] Default hostname changed to ${newHostname}`); this.params.whost = new URL(newHostname).origin; return await this.checkLogin(); } const rdata = await req.body.json(); if(rdata.errno === 0){ this.params.account_id = rdata.uk; } return rdata; } catch(error){ throw new Error('checkLogin', { cause: error }); } } /** * Initiates the pre-login step for passport authentication * @param {string} email - The user's email address * @returns {Promise<Object>} The pre-login data JSON (includes seval, random, timestamp) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async passportPreLogin(email){ const url = new URL(this.params.whost + '/passport/prelogin'); const authUrl = 'wap/outlogin/login'; try{ if(this.data.pcftoken === ''){ await this.updateAppData(authUrl); } const formData = new this.FormUrlEncoded(); formData.append('client', 'web'); formData.append('pass_version', '2.8'); formData.append('clientfrom', 'h5'); formData.append('pcftoken', this.data.pcftoken); formData.append('email', email); const req = await request(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, Referer: this.params.whost, }, body: formData.str(), signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); return rdata; } catch (error) { throw new Error('passportPreLogin', { cause: error }); } } /** * Completes the passport login process using preLoginData and password * @param {Object} preLoginData - Data returned from passportPreLogin (seval, random, timestamp) * @param {string} email - The user's email address * @param {string} pass - The user's plaintext password * @returns {Promise<Object>} The login response JSON (includes ndus token on success) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async passportLogin(preLoginData, email, pass){ const url = new URL(this.params.whost + '/passport/login'); try{ if(this.data.pubkey === ''){ await this.getPublicKey(); } const cJar = new CookieJar(); this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost)); const browserid = cJar.toJSON().cookies.find(c => c.key === 'browserid').value || ''; const encpwd = this.ChangeBase64Type(this.EncryptRSA(pass, this.data.pubkey, 2)); const prand = this.PRandGen('web', preLoginData.seval, encpwd, email, browserid, preLoginData.random); const formData = new this.FormUrlEncoded(); formData.append('client', 'web'); formData.append('pass_version', '2.8'); formData.append('clientfrom', 'h5'); formData.append('pcftoken', this.data.pcftoken); formData.append('prand', prand); formData.append('email', email); formData.append('pwd', encpwd); formData.append('seval', preLoginData.seval); formData.append('random', preLoginData.random); formData.append('timestamp', preLoginData.timestamp); const req = await request(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, Referer: this.params.whost, }, body: formData.str(), signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); if(rdata.code === 0){ if(typeof req.headers['set-cookie'] === 'string'){ req.headers['set-cookie'] = [req.headers['set-cookie']]; } for(const cookie of req.headers['set-cookie']){ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost); } const ndus = cJar.toJSON().cookies.find(c => c.key === 'ndus').value; rdata.data.ndus = ndus; } return rdata; } catch (error) { throw new Error('passportLogin', { cause: error }); } } /** * Sends a registration code to the specified email * @param {string} email - The email address to send the code to * @returns {Promise<Object>} The send code response JSON (includes code and message) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async regSendCode(email){ const url = new URL(this.params.whost + '/passport/register_v4/sendcode'); const emailRegUrl = 'wap/outlogin/emailRegister'; try{ if(this.data.pcftoken === ''){ await this.updateAppData(emailRegUrl); } const formData = new this.FormUrlEncoded(); formData.append('client', 'web'); formData.append('pass_version', '2.8'); formData.append('clientfrom', 'h5'); formData.append('pcftoken', this.data.pcftoken); formData.append('email', email); const req = await request(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, Referer: this.params.whost, }, body: formData.str(), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); // rdata.code: 0 - OK // rdata.code: 10 - Email format invalid // rdata.code: 11 - Email has been register before // rdata.code: 60 - Send code too fast, wait ~60sec return rdata; } catch (error) { throw new Error('regSendCode', { cause: error }); } } /** * Verifies the registration code received via email * @param {string} regToken - Registration token from send code response * @param {string|number} code - The verification code sent to email * @returns {Promise<Object>} The verification response JSON * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async regVerify(regToken, code){ const url = new URL(this.params.whost + '/passport/register_v4/verify'); try{ const formData = new this.FormUrlEncoded(); formData.append('client', 'web'); formData.append('pass_version', '2.8'); formData.append('clientfrom', 'h5'); formData.append('pcftoken', this.data.pcftoken); formData.append('token', regToken); formData.append('code', code); const req = await request(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, Referer: this.params.whost, }, body: formData.str(), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); // rdata.code: 0 - OK // rdata.code: 59 - Email code is wrong return rdata; } catch (error) { throw new Error('regVerify', { cause: error }); } } /** * Completes the registration process by setting a password * @param {string} regToken - Registration token from verification step * @param {string} pass - The new password to set, length is 6-15 and contains at least 1 Latin letter * @returns {Promise<Object>} The finish registration response JSON (includes ndus token on success) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async regFinish(regToken, pass){ const url = new URL(this.params.whost + '/passport/register_v4/finish'); try{ if(this.data.pubkey === ''){ await this.getPublicKey(); } if(typeof pass !== 'string' || pass.length < 6 || pass.length > 15 || !pass.match(/[a-z]/i)){ return { code: -2, logid: 0, msg: 'invalid password', }; } const encpwd = this.ChangeBase64Type(this.EncryptRSA(pass, this.data.pubkey, 2)); const formData = new this.FormUrlEncoded(); formData.append('client', 'web'); formData.append('pass_version', '2.8'); formData.append('clientfrom', 'h5'); formData.append('pcftoken', this.data.pcftoken); formData.append('token', regToken); formData.append('pwd', encpwd); const req = await request(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, Referer: this.params.whost, }, body: formData.str(), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); if(rdata.code === 0 && req.headers['set-cookie']){ const cJar = new CookieJar(); if(typeof req.headers['set-cookie'] === 'string'){ req.headers['set-cookie'] = [req.headers['set-cookie']]; } for(const cookie of req.headers['set-cookie']){ cJar.setCookieSync(cookie.split('; ')[0], this.params.whost); } const ndus = cJar.toJSON().cookies.find(c => c.key === 'ndus').value; rdata.data.ndus = ndus; } return rdata; } catch (error) { throw new Error('regFinish', { cause: error }); } } /** * Retrieves passport user information for the current session * @returns {Promise<Object>} The passport user info JSON (includes display_name) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async passportGetInfo(){ const url = new URL(this.params.whost + '/passport/get_info'); try{ const req = await request(url, { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); if(rdata.errno === 0){ this.params.account_name = rdata.data.display_name; } return rdata; } catch (error) { throw new Error('getPassport', { cause: error }); } } /** * Fetches membership information for the current user * @returns {Promise<Object>} The membership JSON (includes VIP status) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async userMembership(){ const url = new URL(this.params.whost + '/rest/2.0/membership/proxy/user'); url.search = new URLSearchParams({ method: 'query', }); try{ const req = await request(url, { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); if(rdata.error_code === 0){ this.params.is_vip = rdata.data.member_info.is_vip > 0 ? true : false; // this.params.vip_type = this.params.is_vip ? 2 : 0; if(this.params.is_vip === 0){ this.params.vip_type = 0; } } return rdata; } catch(error){ throw new Error('userMembership', { cause: error }); } } /** * Retrieves current user information (username, VIP status) * @returns {Promise<Object>} The user info JSON (includes records array) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async getCurrentUserInfo(){ try{ if(this.params.account_id === 0){ await this.checkLogin(); } const curUser = await this.getUserInfo(this.params.account_id); if(curUser.records.length > 0){ const thisUser = curUser.records[0]; this.params.account_name = thisUser.uname; this.params.is_vip = thisUser.vip_type > 0 ? true : false; this.params.vip_type = thisUser.vip_type; } return curUser; } catch (error) { throw new Error('getCurrentUserInfo', { cause: error }); } } /** * Retrieves information for a specific user ID * @param {number|string} user_id - The user ID to look up * @returns {Promise<Object>} The user info JSON (includes data) * @async * @throws {Error} Throws error if user_id is invalid, HTTP status is not 200, or request fails */ async getUserInfo(user_id){ user_id = parseInt(user_id); const url = new URL(this.params.whost + '/api/user/getinfo'); url.search = new URLSearchParams({ user_list: JSON.stringify([user_id]), need_relation: 0, need_secret_info: 1, }); try{ if(isNaN(user_id) || !Number.isSafeInteger(user_id)){ throw new Error(`${user_id} is not user id`); } const req = await request(url, { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); return rdata; } catch (error) { throw new Error('getUserInfo', { cause: error }); } } /** * Retrieves storage quota information for the current account * @returns {Promise<Object>} The quota JSON (includes total, used, available) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async getQuota(){ const url = new URL(this.params.whost + '/api/quota'); url.search = new URLSearchParams({ checkexpire: 1, checkfree: 1, }); try{ const req = await request(url, { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); if(rdata.errno === 0){ rdata.available = rdata.total - rdata.used; this.params.space_available = rdata.available; this.params.space_total = rdata.total; this.params.space_used = rdata.used; } return rdata; } catch (error) { throw new Error('getQuota', { cause: error }); } } /** * Retrieves the user's coins count (points) * @returns {Promise<Object>} The coins count JSON (includes records of coin usage) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async getCoinsCount(){ const url = new URL(this.params.whost + '/rest/1.0/inte/system/getrecord'); try{ const req = await request(url, { headers: { 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); return rdata; } catch (error) { throw new Error('getCoinsCount', { cause: error }); } } /** * Retrieves the contents of a remote directory * @param {string} remoteDir - Remote directory path to list * @param {number} [page=1] - Page number for pagination * @returns {Promise<Object>} The directory listing JSON (includes entries array) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async getRemoteDir(remoteDir, page = 1){ const url = new URL(this.params.whost + '/api/list'); try{ const formData = new this.FormUrlEncoded(); formData.append('order', 'name'); formData.append('desc', 0); formData.append('dir', remoteDir); formData.append('num', 20000); formData.append('page', page); formData.append('showempty', 0); const req = await request(url, { method: 'POST', body: formData.str(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); return rdata; } catch (error) { throw new Error('getRemoteDir', { cause: error }); } } /** * Retrieves the contents of a remote directory with specific file category * @param {number} [categoryId=1] - selected category: * 1 - video * 2 - audio * 3 - pictures * 4 - documents * 5 - apps * 6 - other * 7 - torrent * @param {string} remoteDir - Remote directory path to list * @param {number} [page=1] - Page number for pagination * @returns {Promise<Object>} The directory listing JSON (includes entries array) * @async * @throws {Error} Throws error if HTTP status is not 200 or request fails */ async getCategoryList(categoryId = 1, remoteDir = '/', page = 1, order = 'name', desc = 0, num = 20000){ const url = new URL(this.params.whost + '/api/categorylist'); try{ const formData = new this.FormUrlEncoded(); formData.append('order', order); formData.append('desc', desc); formData.append('dir', remoteDir); formData.append('num', num); formData.append('page', page); formData.append('category', categoryId); const req = await request(url, { method: 'POST', body: formData.str(), headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.params.ua, 'Cookie': this.params.cookie, }, signal: AbortSignal.timeout(this.TERABOX_TIMEOUT), }); if (req.statusCode !== 200) { throw new Error(`HTTP error! Status: ${req.statusCode}`); } const rdata = await req.body.json(); retur