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
JavaScript
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