@chahakshah/terabox-api 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/api.js +2465 -0
- package/helper.js +363 -0
- package/index.d.ts +434 -0
- package/package.json +38 -0
package/api.js
ADDED
|
@@ -0,0 +1,2465 @@
|
|
|
1
|
+
import { FormData, Client, buildConnector, request } from 'undici';
|
|
2
|
+
import { Cookie, CookieJar } from 'tough-cookie';
|
|
3
|
+
import { filesize } from 'filesize';
|
|
4
|
+
|
|
5
|
+
import child_process from 'node:child_process';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import tls from 'node:tls';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Main module for api interacting with TeraBox
|
|
11
|
+
* @module api
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Constructs a remote file path by combining a directory and filename, ensuring proper slash formatting
|
|
16
|
+
* @param {string} sdir - The directory path (with or without trailing slash)
|
|
17
|
+
* @param {string} sfile - The filename to append to the directory path
|
|
18
|
+
* @returns {string} The combined full path with exactly one slash between directory and filename
|
|
19
|
+
* @example
|
|
20
|
+
* makeRemoteFPath('documents', 'file.txt') // returns 'documents/file.txt'
|
|
21
|
+
* makeRemoteFPath('documents/', 'file.txt') // returns 'documents/file.txt'
|
|
22
|
+
* @ignore
|
|
23
|
+
*/
|
|
24
|
+
function makeRemoteFPath(sdir, sfile){
|
|
25
|
+
const tdir = sdir.match(/\/$/) ? sdir : sdir + '/';
|
|
26
|
+
return tdir + sfile;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A utility class for handling application/x-www-form-urlencoded data
|
|
31
|
+
* Wraps URLSearchParams with additional convenience methods and encoding behavior
|
|
32
|
+
* @class
|
|
33
|
+
*/
|
|
34
|
+
class FormUrlEncoded {
|
|
35
|
+
/**
|
|
36
|
+
* Creates a new FormUrlEncoded instance
|
|
37
|
+
* @param {Object.<string, string>} [params] - Optional initial parameters as key-value pairs
|
|
38
|
+
* @example
|
|
39
|
+
* const form = new FormUrlEncoded({ foo: 'bar', baz: 'qux' });
|
|
40
|
+
*/
|
|
41
|
+
constructor(params) {
|
|
42
|
+
this.data = new URLSearchParams();
|
|
43
|
+
if(typeof params === 'object' && params !== null){
|
|
44
|
+
for (const [key, value] of Object.entries(params)) {
|
|
45
|
+
this.data.append(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sets or replaces a parameter value
|
|
51
|
+
* @param {string} param - The parameter name
|
|
52
|
+
* @param {string} value - The parameter value
|
|
53
|
+
* @returns {void}
|
|
54
|
+
*/
|
|
55
|
+
set(param, value){
|
|
56
|
+
this.data.set(param, value);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Appends a new value to an existing parameter
|
|
60
|
+
* @param {string} param - The parameter name
|
|
61
|
+
* @param {string} value - The parameter value
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
append(param, value){
|
|
65
|
+
this.data.append(param, value);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Removes a parameter
|
|
69
|
+
* @param {string} param - The parameter name to remove
|
|
70
|
+
* @returns {void}
|
|
71
|
+
*/
|
|
72
|
+
delete(param){
|
|
73
|
+
this.data.delete(param);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns the encoded string representation (space encoded as %20)
|
|
77
|
+
* Suitable for application/x-www-form-urlencoded content
|
|
78
|
+
* @returns {string} The encoded form data
|
|
79
|
+
* @example
|
|
80
|
+
* form.str(); // returns "foo=bar&baz=qux"
|
|
81
|
+
*/
|
|
82
|
+
str(){
|
|
83
|
+
return this.data.toString().replace(/\+/g, '%20');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns the underlying URLSearchParams object
|
|
87
|
+
* @returns {URLSearchParams} The native URLSearchParams instance
|
|
88
|
+
*/
|
|
89
|
+
url(){
|
|
90
|
+
return this.data;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generates a signed download token using a modified RC4-like algorithm
|
|
96
|
+
*
|
|
97
|
+
* This function implements a stream cipher similar to RC4 that:
|
|
98
|
+
* <br>1. Initializes a permutation array using the secret key (s1)
|
|
99
|
+
* <br>2. Generates a pseudorandom keystream
|
|
100
|
+
* <br>3. XORs the input data (s2) with the keystream
|
|
101
|
+
* <br>4. Returns the result as a Base64-encoded string
|
|
102
|
+
*
|
|
103
|
+
* @param {string} s1 - The secret key used for signing (should be at least 1 character)
|
|
104
|
+
* @param {string} s2 - The input data to be signed
|
|
105
|
+
* @returns {string} Base64-encoded signature
|
|
106
|
+
* @example
|
|
107
|
+
* const signature = signDownload('secret-key', 'data-to-sign');
|
|
108
|
+
* // Returns something like: "X3p8YFJjUA=="
|
|
109
|
+
*/
|
|
110
|
+
function signDownload(s1, s2) {
|
|
111
|
+
// Initialize permutation array (p) and key array (a)
|
|
112
|
+
const p = new Uint8Array(256);
|
|
113
|
+
const a = new Uint8Array(256);
|
|
114
|
+
const result = [];
|
|
115
|
+
|
|
116
|
+
// Key-scheduling algorithm (KSA)
|
|
117
|
+
// Initialize the permutation array with the secret key
|
|
118
|
+
Array.from({ length: 256 }, (_, i) => {
|
|
119
|
+
a[i] = s1.charCodeAt(i % s1.length);
|
|
120
|
+
p[i] = i;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Scramble the permutation array using the key
|
|
124
|
+
let j = 0;
|
|
125
|
+
Array.from({ length: 256 }, (_, i) => {
|
|
126
|
+
j = (j + p[i] + a[i]) % 256;
|
|
127
|
+
[p[i], p[j]] = [p[j], p[i]]; // swap
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Pseudo-random generation algorithm (PRGA)
|
|
131
|
+
// Generate keystream and XOR with input data
|
|
132
|
+
let i = 0; j = 0;
|
|
133
|
+
Array.from({ length: s2.length }, (_, q) => {
|
|
134
|
+
i = (i + 1) % 256;
|
|
135
|
+
j = (j + p[i]) % 256;
|
|
136
|
+
[p[i], p[j]] = [p[j], p[i]]; // swap
|
|
137
|
+
const k = p[(p[i] + p[j]) % 256];
|
|
138
|
+
result.push(s2.charCodeAt(q) ^ k);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Return the result as Base64
|
|
142
|
+
return Buffer.from(result).toString('base64');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validates whether a string is a properly formatted MD5 hash
|
|
147
|
+
* <br>
|
|
148
|
+
* <br>Checks if the input:
|
|
149
|
+
* <br>1. Is exactly 32 characters long
|
|
150
|
+
* <br>2. Contains only hexadecimal characters (a-f, 0-9)
|
|
151
|
+
* <br>3. Is in lowercase
|
|
152
|
+
* <br>
|
|
153
|
+
* <br>Note: This only validates the format, not the cryptographic correctness of the hash.
|
|
154
|
+
*
|
|
155
|
+
* @param {*} md5 - The value to check (typically a string)
|
|
156
|
+
* @returns {boolean} True if the input is a valid MD5 format, false otherwise
|
|
157
|
+
* @example
|
|
158
|
+
* checkMd5val('d41d8cd98f00b204e9800998ecf8427e') // returns true
|
|
159
|
+
* checkMd5val('D41D8CD98F00B204E9800998ECF8427E') // returns false (uppercase)
|
|
160
|
+
* checkMd5val('z41d8cd98f00b204e9800998ecf8427e') // returns false (invalid character)
|
|
161
|
+
* checkMd5val('d41d8cd98f') // returns false (too short)
|
|
162
|
+
*/
|
|
163
|
+
function checkMd5val(md5){
|
|
164
|
+
if(typeof md5 !== 'string') return false;
|
|
165
|
+
return /^[a-f0-9]{32}$/.test(md5);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validates that all elements in an array are properly formatted MD5 hashes
|
|
170
|
+
* <br>
|
|
171
|
+
* <br>Checks if:
|
|
172
|
+
* <br>1. The input is an array
|
|
173
|
+
* <br>2. Every element in the array passes checkMd5val() validation
|
|
174
|
+
* <br>(32-character hexadecimal strings in lowercase)
|
|
175
|
+
*
|
|
176
|
+
* @param {*} arr - The array to validate
|
|
177
|
+
* @returns {boolean} True if all elements are valid MD5 hashes, false otherwise
|
|
178
|
+
* (also returns false if input is not an array)
|
|
179
|
+
* @see {@link module:api~checkMd5val|Function CheckMd5Val} for individual MD5 hash validation logic
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* checkMd5arr(['d41d8cd98f00b204e9800998ecf8427e', '5d41402abc4b2a76b9719d911017c592']) // true
|
|
183
|
+
* checkMd5arr(['d41d8cd98f00b204e9800998ecf8427e', 'invalid']) // false
|
|
184
|
+
* checkMd5arr('not an array') // false
|
|
185
|
+
* checkMd5arr([]) // false (empty array is considered invalid)
|
|
186
|
+
*/
|
|
187
|
+
function checkMd5arr(arr) {
|
|
188
|
+
if (!Array.isArray(arr)) return false;
|
|
189
|
+
if (arr.length === 0) return false;
|
|
190
|
+
return arr.every(item => {
|
|
191
|
+
return checkMd5val(item);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Applies a custom transformation to what appears to be an MD5 hash
|
|
197
|
+
* <br>
|
|
198
|
+
* <br>This function performs a series of reversible transformations on an input string
|
|
199
|
+
* <br>that appears to be an MD5 hash (32 hexadecimal characters). The transformation includes:
|
|
200
|
+
* <br>1. Character restoration at position 9
|
|
201
|
+
* <br>2. XOR operation with position-dependent values
|
|
202
|
+
* <br>3. Byte reordering of the result
|
|
203
|
+
*
|
|
204
|
+
* @param {string} md5 - The input string (expected to be 32 hexadecimal characters)
|
|
205
|
+
* @returns {string} The transformed result (32 hexadecimal characters)
|
|
206
|
+
* @throws Will return the original input unchanged if length is not 32
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* decodeMd5('a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6') // returns transformed value
|
|
210
|
+
* decodeMd5('short') // returns 'short' (unchanged)
|
|
211
|
+
*/
|
|
212
|
+
function decodeMd5(md5) {
|
|
213
|
+
// Return unchanged if not 32 characters
|
|
214
|
+
if (md5.length !== 32) return md5;
|
|
215
|
+
|
|
216
|
+
// Restore character at position 9
|
|
217
|
+
const restoredHexChar = (md5.charCodeAt(9) - 'g'.charCodeAt(0)).toString(16);
|
|
218
|
+
const o = md5.slice(0, 9) + restoredHexChar + md5.slice(10);
|
|
219
|
+
|
|
220
|
+
// Apply XOR transformation to each character
|
|
221
|
+
let n = '';
|
|
222
|
+
for (let i = 0; i < o.length; i++) {
|
|
223
|
+
const orig = parseInt(o[i], 16) ^ (i & 15);
|
|
224
|
+
n += orig.toString(16);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Reorder the bytes in the result
|
|
228
|
+
const e =
|
|
229
|
+
n.slice(8, 16) + // original bytes 8-15 (now first)
|
|
230
|
+
n.slice(0, 8) + // original bytes 0-7 (now second)
|
|
231
|
+
n.slice(24, 32) + // original bytes 24-31 (now third)
|
|
232
|
+
n.slice(16, 24); // original bytes 16-23 (now last)
|
|
233
|
+
|
|
234
|
+
return e;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Converts between standard and URL-safe Base64 encoding formats
|
|
239
|
+
* <br>
|
|
240
|
+
* <br>Base64 strings may contain '+', '/' and '=' characters that need to be replaced
|
|
241
|
+
* <br>for safe use in URLs. This function provides bidirectional conversion:
|
|
242
|
+
* <br>- Mode 1: Converts to URL-safe Base64 (RFC 4648 §5)
|
|
243
|
+
* <br>- Mode 2: Converts back to standard Base64
|
|
244
|
+
*
|
|
245
|
+
* @param {string} str - The Base64 string to convert
|
|
246
|
+
* @param {number} [mode=1] - Conversion direction:
|
|
247
|
+
* 1 = to URL-safe (default),
|
|
248
|
+
* 2 = to standard
|
|
249
|
+
* @returns {string} The converted Base64 string
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* // To URL-safe Base64
|
|
253
|
+
* changeBase64Type('a+b/c=') // returns 'a-b_c='
|
|
254
|
+
*
|
|
255
|
+
* // To standard Base64
|
|
256
|
+
* changeBase64Type('a-b_c=', 2) // returns 'a+b/c='
|
|
257
|
+
*
|
|
258
|
+
* @see {@link https://tools.ietf.org/html/rfc4648#section-5|RFC 4648 §5} for URL-safe Base64
|
|
259
|
+
*/
|
|
260
|
+
function changeBase64Type(str, mode = 1) {
|
|
261
|
+
return mode === 1
|
|
262
|
+
? str.replace(/\+/g, '-').replace(/\//g, '_') // to url-safe
|
|
263
|
+
: str.replace(/-/g, '+').replace(/_/g, '/'); // to standard
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Decrypts AES-128-CBC encrypted data using provided parameters
|
|
268
|
+
* <br>
|
|
269
|
+
* <br>This function:
|
|
270
|
+
* <br>1. Converts both parameters from URL-safe Base64 to standard Base64
|
|
271
|
+
* <br>2. Extracts the IV (first 16 bytes) and ciphertext from pp1
|
|
272
|
+
* <br>3. Uses pp2 as the decryption key
|
|
273
|
+
* <br>4. Performs AES-128-CBC decryption
|
|
274
|
+
*
|
|
275
|
+
* @param {string} pp1 - Combined IV and ciphertext in URL-safe Base64 format:
|
|
276
|
+
* First 16 bytes are IV, remainder is ciphertext
|
|
277
|
+
* @param {string} pp2 - Encryption key in URL-safe Base64 format
|
|
278
|
+
* @returns {string} The decrypted UTF-8 string
|
|
279
|
+
* @throws {Error} May throw errors for invalid inputs or decryption failures
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* // Example usage (with actual encrypted data)
|
|
283
|
+
* const decrypted = decryptAES(
|
|
284
|
+
* 'MTIzNDU2Nzg5MDEyMzQ1Ng==...', // IV + ciphertext
|
|
285
|
+
* 'c2VjcmV0LWtleS1kYXRhCg==' // Key
|
|
286
|
+
* );
|
|
287
|
+
*
|
|
288
|
+
* @requires crypto Node.js crypto module
|
|
289
|
+
* @see {@link module:api~changeBase64Type|Function ChangeBase64Type} for Base64 format conversion
|
|
290
|
+
*/
|
|
291
|
+
function decryptAES(pp1, pp2) {
|
|
292
|
+
// Convert from URL-safe Base64 to standard Base64
|
|
293
|
+
pp1 = changeBase64Type(pp1, 2);
|
|
294
|
+
pp2 = changeBase64Type(pp2, 2);
|
|
295
|
+
|
|
296
|
+
// Extract ciphertext (after first 16 bytes) and IV (first 16 bytes)
|
|
297
|
+
const cipherText = pp1.substring(16);
|
|
298
|
+
const key = Buffer.from(pp2, 'utf8');
|
|
299
|
+
const iv = Buffer.from(pp1.substring(0, 16), 'utf8');
|
|
300
|
+
|
|
301
|
+
// Create decipher with AES-128-CBC algorithm
|
|
302
|
+
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
|
303
|
+
|
|
304
|
+
// Perform decryption
|
|
305
|
+
let decrypted = decipher.update(cipherText, 'base64', 'utf8');
|
|
306
|
+
decrypted += decipher.final('utf8');
|
|
307
|
+
|
|
308
|
+
return decrypted;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Encrypts data using RSA with a public key, with optional MD5 preprocessing
|
|
313
|
+
* <br>
|
|
314
|
+
* <br>Supports two encryption modes:
|
|
315
|
+
* <br>1. Direct encryption of the message (default)
|
|
316
|
+
* <br>2. MD5 hash preprocessing (applies MD5 + length padding before encryption)
|
|
317
|
+
*
|
|
318
|
+
* @param {string} message - The plaintext message to encrypt
|
|
319
|
+
* @param {string|Buffer} publicKeyPEM - RSA public key in PEM format
|
|
320
|
+
* @param {number} [mode=1] - Encryption mode:
|
|
321
|
+
* 1 = direct encryption,
|
|
322
|
+
* 2 = MD5 hash preprocessing
|
|
323
|
+
* @returns {string} Base64-encoded encrypted data
|
|
324
|
+
* @throws {Error} May throw errors for invalid keys or encryption failures
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* // Direct encryption
|
|
328
|
+
* encryptRSA('secret message', publicKey);
|
|
329
|
+
*
|
|
330
|
+
* // With MD5 preprocessing
|
|
331
|
+
* encryptRSA('secret message', publicKey, 2);
|
|
332
|
+
*
|
|
333
|
+
* @requires crypto Node.js crypto module
|
|
334
|
+
*/
|
|
335
|
+
function encryptRSA(message, publicKeyPEM, mode = 1) {
|
|
336
|
+
// Mode 2: Apply MD5 hash and length padding
|
|
337
|
+
if (mode === 2) {
|
|
338
|
+
const md5 = crypto.createHash('md5').update(message).digest('hex');
|
|
339
|
+
message = md5 + (md5.length<10?'0':'') + md5.length;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Convert message to Buffer
|
|
343
|
+
const buffer = Buffer.from(message, 'utf8');
|
|
344
|
+
|
|
345
|
+
// Perform RSA encryption
|
|
346
|
+
const encrypted = crypto.publicEncrypt({
|
|
347
|
+
key: publicKeyPEM,
|
|
348
|
+
padding: crypto.constants.RSA_PKCS1_PADDING,
|
|
349
|
+
},
|
|
350
|
+
buffer,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// Return as Base64 string
|
|
354
|
+
return encrypted.toString('base64');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Generates a pseudo-random SHA-1 hash from combined client parameters
|
|
359
|
+
* <br>
|
|
360
|
+
* <br>Creates a deterministic hash value by combining multiple client-specific parameters.
|
|
361
|
+
* <br>This is typically used for generating session tokens or unique identifiers.
|
|
362
|
+
*
|
|
363
|
+
* @param {string} [client='web'] - Client identifier (e.g., 'web', 'mobile')
|
|
364
|
+
* @param {string} seval - Session evaluation parameter
|
|
365
|
+
* @param {string} encpwd - Encrypted password or password hash
|
|
366
|
+
* @param {string} email - User's email address
|
|
367
|
+
* @param {string} [browserid=''] - Browser fingerprint or identifier
|
|
368
|
+
* @param {string} random - Random value
|
|
369
|
+
* @returns {string} SHA-1 hash of the combined parameters (40-character hex string)
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* // Basic usage
|
|
373
|
+
* const token = prandGen('web', 'session123', 'encryptedPwd', 'user@example.com', 'browser123', 'randomValue');
|
|
374
|
+
*
|
|
375
|
+
* // With default client and empty browserid
|
|
376
|
+
* const token = prandGen(undefined, 'session123', 'encryptedPwd', 'user@example.com', '', 'randomValue');
|
|
377
|
+
*
|
|
378
|
+
* @requires crypto Node.js crypto module
|
|
379
|
+
*/
|
|
380
|
+
function prandGen(client = 'web', seval, encpwd, email, browserid = '', random) {
|
|
381
|
+
// Combine all parameters with hyphens
|
|
382
|
+
const combined = `${client}-${seval}-${encpwd}-${email}-${browserid}-${random}`;
|
|
383
|
+
|
|
384
|
+
// Generate SHA-1 hash and return as hex string
|
|
385
|
+
return crypto.createHash('sha1').update(combined).digest('hex');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* TeraBoxApp API client class
|
|
390
|
+
*
|
|
391
|
+
* Provides a comprehensive interface for interacting with TeraBox services,
|
|
392
|
+
* including encryption utilities, API request handling, and session management.
|
|
393
|
+
*
|
|
394
|
+
* @class
|
|
395
|
+
* @property {module:api~FormUrlEncoded } FormUrlEncoded - Form URL encoding utility
|
|
396
|
+
* @property {module:api~signDownload } SignDownload - Download signature generator
|
|
397
|
+
* @property {module:api~checkMd5val } CheckMd5Val - MD5 hash validator (single)
|
|
398
|
+
* @property {module:api~checkMd5arr } CheckMd5Arr - MD5 hash validator (array)
|
|
399
|
+
* @property {module:api~decodeMd5 } DecodeMd5 - Custom MD5 transformation
|
|
400
|
+
* @property {module:api~changeBase64Type } ChangeBase64Type - Base64 format converter
|
|
401
|
+
* @property {module:api~decryptAES } DecryptAES - AES decryption utility
|
|
402
|
+
* @property {module:api~encryptRSA } EncryptRSA - RSA encryption utility
|
|
403
|
+
* @property {module:api~prandGen } PRandGen - Pseudo-random hash generator
|
|
404
|
+
*
|
|
405
|
+
* @property {string} TERABOX_DOMAIN - Default TeraBox domain
|
|
406
|
+
* @property {number} TERABOX_TIMEOUT - Default API timeout (10 seconds)
|
|
407
|
+
*
|
|
408
|
+
* @property {Object} data - Application data including tokens and keys
|
|
409
|
+
* @property {string} data.csrf - CSRF token
|
|
410
|
+
* @property {string} data.logid - Log ID
|
|
411
|
+
* @property {string} data.pcftoken - PCF token
|
|
412
|
+
* @property {string} data.bdstoken - BDS token
|
|
413
|
+
* @property {string} data.jsToken - JavaScript token
|
|
414
|
+
* @property {string} data.pubkey - Public key
|
|
415
|
+
*
|
|
416
|
+
* @property {TeraBoxAppParams} params - Application parameters and configuration
|
|
417
|
+
*/
|
|
418
|
+
class TeraBoxApp {
|
|
419
|
+
// Encryption/Utility Methods 1
|
|
420
|
+
FormUrlEncoded = FormUrlEncoded;
|
|
421
|
+
SignDownload = signDownload;
|
|
422
|
+
CheckMd5Val = checkMd5val;
|
|
423
|
+
CheckMd5Arr = checkMd5arr;
|
|
424
|
+
DecodeMd5 = decodeMd5;
|
|
425
|
+
|
|
426
|
+
// Encryption/Utility Methods 2
|
|
427
|
+
ChangeBase64Type = changeBase64Type;
|
|
428
|
+
DecryptAES = decryptAES;
|
|
429
|
+
EncryptRSA = encryptRSA;
|
|
430
|
+
PRandGen = prandGen;
|
|
431
|
+
|
|
432
|
+
// Constants
|
|
433
|
+
TERABOX_DOMAIN = 'terabox.com';
|
|
434
|
+
TERABOX_TIMEOUT = 10000;
|
|
435
|
+
|
|
436
|
+
// app data
|
|
437
|
+
data = {
|
|
438
|
+
csrf: '',
|
|
439
|
+
logid: '0',
|
|
440
|
+
pcftoken: '',
|
|
441
|
+
bdstoken: '',
|
|
442
|
+
jsToken: '',
|
|
443
|
+
pubkey: '',
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Application parameters and configuration
|
|
447
|
+
params = {
|
|
448
|
+
whost: 'https://jp.' + this.TERABOX_DOMAIN,
|
|
449
|
+
uhost: 'https://c-jp.' + this.TERABOX_DOMAIN,
|
|
450
|
+
lang: 'en',
|
|
451
|
+
app: {
|
|
452
|
+
app_id: 250528,
|
|
453
|
+
web: 1,
|
|
454
|
+
channel: 'dubox',
|
|
455
|
+
clienttype: 0, // 5 is wap?
|
|
456
|
+
},
|
|
457
|
+
ver_android: '3.44.2',
|
|
458
|
+
ua: 'terabox;1.40.0.132;PC;PC-Windows;10.0.26100;WindowsTeraBox',
|
|
459
|
+
cookie: '',
|
|
460
|
+
auth: {},
|
|
461
|
+
account_id: 0,
|
|
462
|
+
account_name: '',
|
|
463
|
+
is_vip: false,
|
|
464
|
+
vip_type: 0,
|
|
465
|
+
space_used: 0,
|
|
466
|
+
space_total: Math.pow(1024, 3),
|
|
467
|
+
space_available: Math.pow(1024, 3),
|
|
468
|
+
cursor: 'null',
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Creates a new TeraBoxApp instance
|
|
473
|
+
* @param {string} authData - Authentication data (NDUS token)
|
|
474
|
+
* @param {string} [authType='ndus'] - Authentication type (currently only 'ndus' supported)
|
|
475
|
+
* @throws {Error} Throws error if authType is not supported
|
|
476
|
+
*/
|
|
477
|
+
constructor(authData, authType = 'ndus') {
|
|
478
|
+
this.params.cookie = `lang=${this.params.lang}`;
|
|
479
|
+
if(authType === 'ndus'){
|
|
480
|
+
this.params.cookie += authData ? '; ndus=' + authData : '';
|
|
481
|
+
}
|
|
482
|
+
else{
|
|
483
|
+
throw new Error('initTBApp', { cause: 'AuthType Not Supported!' });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Updates application data including tokens and user information
|
|
489
|
+
* @param {string} [customPath] - Custom path to use for the update request
|
|
490
|
+
* @param {number} [retries=4] - Number of retry attempts
|
|
491
|
+
* @returns {Promise<Object>} The updated template data
|
|
492
|
+
* @async
|
|
493
|
+
* @throws {Error} Throws error if request fails or parsing fails
|
|
494
|
+
*/
|
|
495
|
+
async updateAppData(customPath, retries = 4){
|
|
496
|
+
const url = new URL(this.params.whost + (customPath ? `/${customPath}` : '/main'));
|
|
497
|
+
|
|
498
|
+
try{
|
|
499
|
+
const req = await request(url, {
|
|
500
|
+
headers:{
|
|
501
|
+
'User-Agent': this.params.ua,
|
|
502
|
+
'Cookie': this.params.cookie,
|
|
503
|
+
},
|
|
504
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT + 10000),
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if(req.statusCode === 302){
|
|
508
|
+
const newUrl = new URL(req.headers.location);
|
|
509
|
+
if(this.params.whost !== newUrl.origin){
|
|
510
|
+
this.params.whost = newUrl.origin;
|
|
511
|
+
console.warn(`[WARN] Default hostname changed to ${newUrl.origin}`);
|
|
512
|
+
}
|
|
513
|
+
const toPathname = newUrl.pathname.replace(/^\//, '');
|
|
514
|
+
const finalUrl = toPathname + newUrl.search;
|
|
515
|
+
return await this.updateAppData(finalUrl, retries);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if(req.headers['set-cookie']){
|
|
519
|
+
const cJar = new CookieJar();
|
|
520
|
+
this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost));
|
|
521
|
+
if(typeof req.headers['set-cookie'] === 'string'){
|
|
522
|
+
req.headers['set-cookie'] = [req.headers['set-cookie']];
|
|
523
|
+
}
|
|
524
|
+
for(const cookie of req.headers['set-cookie']){
|
|
525
|
+
cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
|
|
526
|
+
}
|
|
527
|
+
this.params.cookie = cJar.getCookiesSync(this.params.whost).map(cookie => cookie.cookieString()).join('; ');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const rdata = await req.body.text();
|
|
531
|
+
const tdataRegex = /<script>var templateData = (.*);<\/script>/;
|
|
532
|
+
const jsTokenRegex = /window.jsToken%20%3D%20a%7D%3Bfn%28%22(.*)%22%29/;
|
|
533
|
+
const tdata = rdata.match(tdataRegex) ? JSON.parse(rdata.match(tdataRegex)[1].split(';</script>')[0]) : {};
|
|
534
|
+
const isLoginReq = req.headers.location === '/login' ? true : false;
|
|
535
|
+
|
|
536
|
+
if(tdata.jsToken){
|
|
537
|
+
tdata.jsToken = tdata.jsToken.match(/%28%22(.*)%22%29/)[1];
|
|
538
|
+
}
|
|
539
|
+
else if(rdata.match(jsTokenRegex)){
|
|
540
|
+
tdata.jsToken = rdata.match(jsTokenRegex)[1];
|
|
541
|
+
}
|
|
542
|
+
else if(isLoginReq){
|
|
543
|
+
console.error('[ERROR] Failed to update jsToken [Login Required]');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if(req.headers.logid){
|
|
547
|
+
this.data.logid = req.headers.logid;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
this.data.csrf = tdata.csrf || '';
|
|
551
|
+
this.data.pcftoken = tdata.pcftoken || '';
|
|
552
|
+
this.data.bdstoken = tdata.bdstoken || '';
|
|
553
|
+
this.data.jsToken = tdata.jsToken || '';
|
|
554
|
+
|
|
555
|
+
this.params.account_id = parseInt(tdata.uk) || 0;
|
|
556
|
+
if(typeof tdata.userVipIdentity === 'number' && tdata.userVipIdentity > 0){
|
|
557
|
+
this.params.is_vip = true;
|
|
558
|
+
this.params.vip_type = 1;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return tdata;
|
|
562
|
+
}
|
|
563
|
+
catch(error){
|
|
564
|
+
if(error.name === 'TimeoutError' && retries > 0){
|
|
565
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
566
|
+
return await this.updateAppData(customPath, retries - 1);
|
|
567
|
+
}
|
|
568
|
+
const errorPrefix = '[ERROR] Failed to update jsToken:';
|
|
569
|
+
if(error.name === 'TimeoutError'){
|
|
570
|
+
console.error(errorPrefix, error.message);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
error = new Error('updateAppData', { cause: error });
|
|
574
|
+
console.error(errorPrefix, error);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Sets default VIP parameters
|
|
580
|
+
* @returns {void}
|
|
581
|
+
*/
|
|
582
|
+
setVipDefaults(){
|
|
583
|
+
this.params.is_vip = true;
|
|
584
|
+
this.params.vip_type = 1; // 1: VIP, 2: SVIP
|
|
585
|
+
this.params.space_total = Math.pow(1024, 3) * 2;
|
|
586
|
+
this.params.space_available = Math.pow(1024, 3) * 2;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Makes an API request with retry logic
|
|
591
|
+
* @param {string} req_url - The request URL (relative to whost)
|
|
592
|
+
* @param {Object} [req_options={}] - Request options (headers, body, etc.)
|
|
593
|
+
* @param {number} [retries=4] - Number of retry attempts
|
|
594
|
+
* @returns {Promise<Object>} The JSON-parsed response data
|
|
595
|
+
* @async
|
|
596
|
+
* @throws {Error} Throws error if all retries fail
|
|
597
|
+
*/
|
|
598
|
+
async doReq(req_url, req_options = {}, retries = 4){
|
|
599
|
+
const url = new URL(this.params.whost + req_url);
|
|
600
|
+
let reqm_options = structuredClone(req_options);
|
|
601
|
+
let req_headers = {};
|
|
602
|
+
|
|
603
|
+
if(reqm_options.headers){
|
|
604
|
+
req_headers = reqm_options.headers;
|
|
605
|
+
delete reqm_options.headers;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const save_cookies = reqm_options.save_cookies;
|
|
609
|
+
delete reqm_options.save_cookies;
|
|
610
|
+
const silent_retry = reqm_options.silent_retry;
|
|
611
|
+
delete reqm_options.silent_retry;
|
|
612
|
+
const req_timeout = reqm_options.timeout ? reqm_options.timeout : this.TERABOX_TIMEOUT;
|
|
613
|
+
delete reqm_options.timeout;
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
const options = {
|
|
617
|
+
headers: {
|
|
618
|
+
'User-Agent': this.params.ua,
|
|
619
|
+
'Cookie': this.params.cookie,
|
|
620
|
+
...req_headers,
|
|
621
|
+
},
|
|
622
|
+
...reqm_options,
|
|
623
|
+
signal: AbortSignal.timeout(req_timeout),
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const req = await request(url, options);
|
|
627
|
+
|
|
628
|
+
if(save_cookies && req.headers['set-cookie']){
|
|
629
|
+
const cJar = new CookieJar();
|
|
630
|
+
this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost));
|
|
631
|
+
if(typeof req.headers['set-cookie'] === 'string'){
|
|
632
|
+
req.headers['set-cookie'] = [req.headers['set-cookie']];
|
|
633
|
+
}
|
|
634
|
+
for(const cookie of req.headers['set-cookie']){
|
|
635
|
+
cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
|
|
636
|
+
}
|
|
637
|
+
this.params.cookie = cJar.getCookiesSync(this.params.whost).map(cookie => cookie.cookieString()).join('; ');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const rdata = await req.body.json();
|
|
641
|
+
return rdata;
|
|
642
|
+
}
|
|
643
|
+
catch(error){
|
|
644
|
+
if (retries > 0) {
|
|
645
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
646
|
+
if(!silent_retry){
|
|
647
|
+
console.error('[ERROR] DoReq:', req_url, '|', error.code, ':', error.message, '(retrying...)');
|
|
648
|
+
}
|
|
649
|
+
return await this.doReq(req_url, req_options, retries - 1);
|
|
650
|
+
}
|
|
651
|
+
throw new Error('doReq', { cause: error });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Retrieves system configuration from the TeraBox API
|
|
657
|
+
* @returns {Promise<Object>} The system configuration JSON data
|
|
658
|
+
* @async
|
|
659
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
660
|
+
*/
|
|
661
|
+
async getSysCfg(){
|
|
662
|
+
const url = new URL(this.params.whost + '/api/getsyscfg');
|
|
663
|
+
url.search = new URLSearchParams({
|
|
664
|
+
clienttype: this.params.app.clienttype,
|
|
665
|
+
language_type: this.params.lang,
|
|
666
|
+
cfg_category_keys: '[]',
|
|
667
|
+
version: 0,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
try{
|
|
671
|
+
const req = await request(url, {
|
|
672
|
+
headers: {
|
|
673
|
+
'User-Agent': this.params.ua,
|
|
674
|
+
// 'Cookie': this.params.cookie,
|
|
675
|
+
},
|
|
676
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
if (req.statusCode !== 200) {
|
|
680
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const rdata = await req.body.json();
|
|
684
|
+
return rdata;
|
|
685
|
+
}
|
|
686
|
+
catch(error){
|
|
687
|
+
throw new Error('getSysCfg', { cause: error });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Checks login status of the current session.
|
|
693
|
+
* @returns {Promise<CheckLoginResponse>} The login status JSON data.
|
|
694
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails.
|
|
695
|
+
* @async
|
|
696
|
+
*/
|
|
697
|
+
async checkLogin(){
|
|
698
|
+
const url = new URL(this.params.whost + '/api/check/login');
|
|
699
|
+
|
|
700
|
+
try{
|
|
701
|
+
const req = await request(url, {
|
|
702
|
+
headers: {
|
|
703
|
+
'User-Agent': this.params.ua,
|
|
704
|
+
'Cookie': this.params.cookie,
|
|
705
|
+
},
|
|
706
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (req.statusCode !== 200) {
|
|
710
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const regionPrefix = req.headers['region-domain-prefix'];
|
|
714
|
+
if(regionPrefix){
|
|
715
|
+
const newHostname = `https://${regionPrefix}.${this.TERABOX_DOMAIN}`;
|
|
716
|
+
console.warn(`[WARN] Default hostname changed to ${newHostname}`);
|
|
717
|
+
this.params.whost = new URL(newHostname).origin;
|
|
718
|
+
return await this.checkLogin();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const rdata = await req.body.json();
|
|
722
|
+
if(rdata.errno === 0){
|
|
723
|
+
this.params.account_id = rdata.uk;
|
|
724
|
+
}
|
|
725
|
+
return rdata;
|
|
726
|
+
}
|
|
727
|
+
catch(error){
|
|
728
|
+
throw new Error('checkLogin', { cause: error });
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Initiates the pre-login step for passport authentication
|
|
734
|
+
* @param {string} email - The user's email address
|
|
735
|
+
* @returns {Promise<Object>} The pre-login data JSON (includes seval, random, timestamp)
|
|
736
|
+
* @async
|
|
737
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
738
|
+
*/
|
|
739
|
+
async passportPreLogin(email){
|
|
740
|
+
const url = new URL(this.params.whost + '/passport/prelogin');
|
|
741
|
+
const authUrl = 'wap/outlogin/login';
|
|
742
|
+
|
|
743
|
+
try{
|
|
744
|
+
if(this.data.pcftoken === ''){
|
|
745
|
+
await this.updateAppData(authUrl);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const formData = new this.FormUrlEncoded();
|
|
749
|
+
formData.append('client', 'web');
|
|
750
|
+
formData.append('pass_version', '2.8');
|
|
751
|
+
formData.append('clientfrom', 'h5');
|
|
752
|
+
formData.append('pcftoken', this.data.pcftoken);
|
|
753
|
+
formData.append('email', email);
|
|
754
|
+
|
|
755
|
+
const req = await request(url, {
|
|
756
|
+
method: 'POST',
|
|
757
|
+
headers: {
|
|
758
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
759
|
+
'User-Agent': this.params.ua,
|
|
760
|
+
'Cookie': this.params.cookie,
|
|
761
|
+
Referer: this.params.whost,
|
|
762
|
+
},
|
|
763
|
+
body: formData.str(),
|
|
764
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (req.statusCode !== 200) {
|
|
768
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const rdata = await req.body.json();
|
|
772
|
+
return rdata;
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
throw new Error('passportPreLogin', { cause: error });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Completes the passport login process using preLoginData and password
|
|
781
|
+
* @param {Object} preLoginData - Data returned from passportPreLogin
|
|
782
|
+
* @param {string} preLoginData.seval - The seval value from pre-login.
|
|
783
|
+
* @param {string} preLoginData.random - The random value from pre-login.
|
|
784
|
+
* @param {number} preLoginData.timestamp - The timestamp from pre-login.
|
|
785
|
+
* @param {string} email - The user's email address
|
|
786
|
+
* @param {string} pass - The user's plaintext password
|
|
787
|
+
* @returns {Promise<Object>} The login response JSON (includes ndus token on success)
|
|
788
|
+
* @async
|
|
789
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
790
|
+
*/
|
|
791
|
+
async passportLogin(preLoginData, email, pass){
|
|
792
|
+
const url = new URL(this.params.whost + '/passport/login');
|
|
793
|
+
|
|
794
|
+
try{
|
|
795
|
+
if(this.data.pubkey === ''){
|
|
796
|
+
await this.getPublicKey();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const cJar = new CookieJar();
|
|
800
|
+
this.params.cookie.split(';').map(cookie => cJar.setCookieSync(cookie, this.params.whost));
|
|
801
|
+
const browserid = cJar.toJSON().cookies.find(c => c.key === 'browserid').value || '';
|
|
802
|
+
const encpwd = this.ChangeBase64Type(this.EncryptRSA(pass, this.data.pubkey, 2));
|
|
803
|
+
|
|
804
|
+
const prand = this.PRandGen('web', preLoginData.seval, encpwd, email, browserid, preLoginData.random);
|
|
805
|
+
|
|
806
|
+
const formData = new this.FormUrlEncoded();
|
|
807
|
+
formData.append('client', 'web');
|
|
808
|
+
formData.append('pass_version', '2.8');
|
|
809
|
+
formData.append('clientfrom', 'h5');
|
|
810
|
+
formData.append('pcftoken', this.data.pcftoken);
|
|
811
|
+
formData.append('prand', prand);
|
|
812
|
+
formData.append('email', email);
|
|
813
|
+
formData.append('pwd', encpwd);
|
|
814
|
+
formData.append('seval', preLoginData.seval);
|
|
815
|
+
formData.append('random', preLoginData.random);
|
|
816
|
+
formData.append('timestamp', preLoginData.timestamp);
|
|
817
|
+
|
|
818
|
+
const req = await request(url, {
|
|
819
|
+
method: 'POST',
|
|
820
|
+
headers: {
|
|
821
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
822
|
+
'User-Agent': this.params.ua,
|
|
823
|
+
'Cookie': this.params.cookie,
|
|
824
|
+
Referer: this.params.whost,
|
|
825
|
+
},
|
|
826
|
+
body: formData.str(),
|
|
827
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
if (req.statusCode !== 200) {
|
|
831
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const rdata = await req.body.json();
|
|
835
|
+
if(rdata.code === 0){
|
|
836
|
+
if(typeof req.headers['set-cookie'] === 'string'){
|
|
837
|
+
req.headers['set-cookie'] = [req.headers['set-cookie']];
|
|
838
|
+
}
|
|
839
|
+
for(const cookie of req.headers['set-cookie']){
|
|
840
|
+
cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
|
|
841
|
+
}
|
|
842
|
+
const ndus = cJar.toJSON().cookies.find(c => c.key === 'ndus').value;
|
|
843
|
+
rdata.data.ndus = ndus;
|
|
844
|
+
}
|
|
845
|
+
return rdata;
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
throw new Error('passportLogin', { cause: error });
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Sends a registration code to the specified email
|
|
854
|
+
* @param {string} email - The email address to send the code to
|
|
855
|
+
* @returns {Promise<Object>} The send code response JSON (includes code and message)
|
|
856
|
+
* @async
|
|
857
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
858
|
+
*/
|
|
859
|
+
async regSendCode(email){
|
|
860
|
+
const url = new URL(this.params.whost + '/passport/register_v4/sendcode');
|
|
861
|
+
const emailRegUrl = 'wap/outlogin/emailRegister';
|
|
862
|
+
|
|
863
|
+
try{
|
|
864
|
+
if(this.data.pcftoken === ''){
|
|
865
|
+
await this.updateAppData(emailRegUrl);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const formData = new this.FormUrlEncoded();
|
|
869
|
+
formData.append('client', 'web');
|
|
870
|
+
formData.append('pass_version', '2.8');
|
|
871
|
+
formData.append('clientfrom', 'h5');
|
|
872
|
+
formData.append('pcftoken', this.data.pcftoken);
|
|
873
|
+
formData.append('email', email);
|
|
874
|
+
|
|
875
|
+
const req = await request(url, {
|
|
876
|
+
method: 'POST',
|
|
877
|
+
headers: {
|
|
878
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
879
|
+
'User-Agent': this.params.ua,
|
|
880
|
+
'Cookie': this.params.cookie,
|
|
881
|
+
Referer: this.params.whost,
|
|
882
|
+
},
|
|
883
|
+
body: formData.str(),
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
if (req.statusCode !== 200) {
|
|
887
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const rdata = await req.body.json();
|
|
891
|
+
// rdata.code: 0 - OK
|
|
892
|
+
// rdata.code: 10 - Email format invalid
|
|
893
|
+
// rdata.code: 11 - Email has been register before
|
|
894
|
+
// rdata.code: 60 - Send code too fast, wait ~60sec
|
|
895
|
+
return rdata;
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
throw new Error('regSendCode', { cause: error });
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* Verifies the registration code received via email
|
|
904
|
+
* @param {string} regToken - Registration token from send code response
|
|
905
|
+
* @param {string|number} code - The verification code sent to email
|
|
906
|
+
* @returns {Promise<Object>} The verification response JSON
|
|
907
|
+
* @async
|
|
908
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
909
|
+
*/
|
|
910
|
+
async regVerify(regToken, code){
|
|
911
|
+
const url = new URL(this.params.whost + '/passport/register_v4/verify');
|
|
912
|
+
|
|
913
|
+
try{
|
|
914
|
+
const formData = new this.FormUrlEncoded();
|
|
915
|
+
formData.append('client', 'web');
|
|
916
|
+
formData.append('pass_version', '2.8');
|
|
917
|
+
formData.append('clientfrom', 'h5');
|
|
918
|
+
formData.append('pcftoken', this.data.pcftoken);
|
|
919
|
+
formData.append('token', regToken);
|
|
920
|
+
formData.append('code', code);
|
|
921
|
+
|
|
922
|
+
const req = await request(url, {
|
|
923
|
+
method: 'POST',
|
|
924
|
+
headers: {
|
|
925
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
926
|
+
'User-Agent': this.params.ua,
|
|
927
|
+
'Cookie': this.params.cookie,
|
|
928
|
+
Referer: this.params.whost,
|
|
929
|
+
},
|
|
930
|
+
body: formData.str(),
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
if (req.statusCode !== 200) {
|
|
934
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const rdata = await req.body.json();
|
|
938
|
+
// rdata.code: 0 - OK
|
|
939
|
+
// rdata.code: 59 - Email code is wrong
|
|
940
|
+
return rdata;
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
throw new Error('regVerify', { cause: error });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Completes the registration process by setting a password
|
|
949
|
+
* @param {string} regToken - Registration token from verification step
|
|
950
|
+
* @param {string} pass - The new password to set, length is 6-15 and contains at least 1 Latin letter
|
|
951
|
+
* @returns {Promise<Object>} The finish registration response JSON (includes ndus token on success)
|
|
952
|
+
* @async
|
|
953
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
954
|
+
*/
|
|
955
|
+
async regFinish(regToken, pass){
|
|
956
|
+
const url = new URL(this.params.whost + '/passport/register_v4/finish');
|
|
957
|
+
|
|
958
|
+
try{
|
|
959
|
+
if(this.data.pubkey === ''){
|
|
960
|
+
await this.getPublicKey();
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if(typeof pass !== 'string' || pass.length < 6 || pass.length > 15 || !pass.match(/[a-z]/i)){
|
|
964
|
+
return { code: -2, logid: 0, msg: 'invalid password', };
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const encpwd = this.ChangeBase64Type(this.EncryptRSA(pass, this.data.pubkey, 2));
|
|
968
|
+
|
|
969
|
+
const formData = new this.FormUrlEncoded();
|
|
970
|
+
formData.append('client', 'web');
|
|
971
|
+
formData.append('pass_version', '2.8');
|
|
972
|
+
formData.append('clientfrom', 'h5');
|
|
973
|
+
formData.append('pcftoken', this.data.pcftoken);
|
|
974
|
+
formData.append('token', regToken);
|
|
975
|
+
formData.append('pwd', encpwd);
|
|
976
|
+
|
|
977
|
+
const req = await request(url, {
|
|
978
|
+
method: 'POST',
|
|
979
|
+
headers: {
|
|
980
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
981
|
+
'User-Agent': this.params.ua,
|
|
982
|
+
'Cookie': this.params.cookie,
|
|
983
|
+
Referer: this.params.whost,
|
|
984
|
+
},
|
|
985
|
+
body: formData.str(),
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
if (req.statusCode !== 200) {
|
|
989
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const rdata = await req.body.json();
|
|
993
|
+
if(rdata.code === 0 && req.headers['set-cookie']){
|
|
994
|
+
const cJar = new CookieJar();
|
|
995
|
+
|
|
996
|
+
if(typeof req.headers['set-cookie'] === 'string'){
|
|
997
|
+
req.headers['set-cookie'] = [req.headers['set-cookie']];
|
|
998
|
+
}
|
|
999
|
+
for(const cookie of req.headers['set-cookie']){
|
|
1000
|
+
cJar.setCookieSync(cookie.split('; ')[0], this.params.whost);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const ndus = cJar.toJSON().cookies.find(c => c.key === 'ndus').value;
|
|
1004
|
+
rdata.data.ndus = ndus;
|
|
1005
|
+
}
|
|
1006
|
+
return rdata;
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
throw new Error('regFinish', { cause: error });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Retrieves passport user information for the current session
|
|
1015
|
+
* @returns {Promise<Object>} The passport user info JSON (includes display_name)
|
|
1016
|
+
* @async
|
|
1017
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1018
|
+
*/
|
|
1019
|
+
async passportGetInfo(){
|
|
1020
|
+
const url = new URL(this.params.whost + '/passport/get_info');
|
|
1021
|
+
|
|
1022
|
+
try{
|
|
1023
|
+
const req = await request(url, {
|
|
1024
|
+
headers: {
|
|
1025
|
+
'User-Agent': this.params.ua,
|
|
1026
|
+
'Cookie': this.params.cookie,
|
|
1027
|
+
},
|
|
1028
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
if (req.statusCode !== 200) {
|
|
1032
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const rdata = await req.body.json();
|
|
1036
|
+
if(rdata.errno === 0){
|
|
1037
|
+
this.params.account_name = rdata.data.display_name;
|
|
1038
|
+
}
|
|
1039
|
+
return rdata;
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
throw new Error('getPassport', { cause: error });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Fetches membership information for the current user
|
|
1048
|
+
* @returns {Promise<Object>} The membership JSON (includes VIP status)
|
|
1049
|
+
* @async
|
|
1050
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1051
|
+
*/
|
|
1052
|
+
async userMembership(){
|
|
1053
|
+
const url = new URL(this.params.whost + '/rest/2.0/membership/proxy/user');
|
|
1054
|
+
url.search = new URLSearchParams({
|
|
1055
|
+
method: 'query',
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
try{
|
|
1059
|
+
const req = await request(url, {
|
|
1060
|
+
headers: {
|
|
1061
|
+
'User-Agent': this.params.ua,
|
|
1062
|
+
'Cookie': this.params.cookie,
|
|
1063
|
+
},
|
|
1064
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
if (req.statusCode !== 200) {
|
|
1068
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const rdata = await req.body.json();
|
|
1072
|
+
if(rdata.error_code === 0){
|
|
1073
|
+
this.params.is_vip = rdata.data.member_info.is_vip > 0 ? true : false;
|
|
1074
|
+
// this.params.vip_type = this.params.is_vip ? 2 : 0;
|
|
1075
|
+
if(this.params.is_vip === 0){
|
|
1076
|
+
this.params.vip_type = 0;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return rdata;
|
|
1080
|
+
}
|
|
1081
|
+
catch(error){
|
|
1082
|
+
throw new Error('userMembership', { cause: error });
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Retrieves current user information (username, VIP status)
|
|
1088
|
+
* @returns {Promise<Object>} The user info JSON (includes records array)
|
|
1089
|
+
* @async
|
|
1090
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1091
|
+
*/
|
|
1092
|
+
async getCurrentUserInfo(){
|
|
1093
|
+
try{
|
|
1094
|
+
if(this.params.account_id === 0){
|
|
1095
|
+
await this.checkLogin();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
const curUser = await this.getUserInfo(this.params.account_id);
|
|
1099
|
+
if(curUser.records.length > 0){
|
|
1100
|
+
const thisUser = curUser.records[0];
|
|
1101
|
+
this.params.account_name = thisUser.uname;
|
|
1102
|
+
this.params.is_vip = thisUser.vip_type > 0 ? true : false;
|
|
1103
|
+
this.params.vip_type = thisUser.vip_type;
|
|
1104
|
+
}
|
|
1105
|
+
return curUser;
|
|
1106
|
+
}
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
throw new Error('getCurrentUserInfo', { cause: error });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Retrieves information for a specific user ID
|
|
1114
|
+
* @param {number|string} user_id - The user ID to look up
|
|
1115
|
+
* @returns {Promise<Object>} The user info JSON (includes data)
|
|
1116
|
+
* @async
|
|
1117
|
+
* @throws {Error} Throws error if user_id is invalid, HTTP status is not 200, or request fails
|
|
1118
|
+
*/
|
|
1119
|
+
async getUserInfo(user_id){
|
|
1120
|
+
user_id = parseInt(user_id);
|
|
1121
|
+
const url = new URL(this.params.whost + '/api/user/getinfo');
|
|
1122
|
+
url.search = new URLSearchParams({
|
|
1123
|
+
user_list: JSON.stringify([user_id]),
|
|
1124
|
+
need_relation: 0,
|
|
1125
|
+
need_secret_info: 1,
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
try{
|
|
1129
|
+
if(isNaN(user_id) || !Number.isSafeInteger(user_id)){
|
|
1130
|
+
throw new Error(`${user_id} is not user id`);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const req = await request(url, {
|
|
1134
|
+
headers: {
|
|
1135
|
+
'User-Agent': this.params.ua,
|
|
1136
|
+
'Cookie': this.params.cookie,
|
|
1137
|
+
},
|
|
1138
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
if (req.statusCode !== 200) {
|
|
1142
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const rdata = await req.body.json();
|
|
1146
|
+
return rdata;
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
throw new Error('getUserInfo', { cause: error });
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Retrieves storage quota information for the current account
|
|
1155
|
+
* @returns {Promise<Object>} The quota JSON (includes total, used, available)
|
|
1156
|
+
* @async
|
|
1157
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1158
|
+
*/
|
|
1159
|
+
async getQuota(){
|
|
1160
|
+
const url = new URL(this.params.whost + '/api/quota');
|
|
1161
|
+
url.search = new URLSearchParams({
|
|
1162
|
+
checkexpire: 1,
|
|
1163
|
+
checkfree: 1,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
try{
|
|
1167
|
+
const req = await request(url, {
|
|
1168
|
+
headers: {
|
|
1169
|
+
'User-Agent': this.params.ua,
|
|
1170
|
+
'Cookie': this.params.cookie,
|
|
1171
|
+
},
|
|
1172
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
if (req.statusCode !== 200) {
|
|
1176
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const rdata = await req.body.json();
|
|
1180
|
+
if(rdata.errno === 0){
|
|
1181
|
+
rdata.available = rdata.total - rdata.used;
|
|
1182
|
+
this.params.space_available = rdata.available;
|
|
1183
|
+
this.params.space_total = rdata.total;
|
|
1184
|
+
this.params.space_used = rdata.used;
|
|
1185
|
+
}
|
|
1186
|
+
return rdata;
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
throw new Error('getQuota', { cause: error });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Retrieves the user's coins count (points)
|
|
1195
|
+
* @returns {Promise<Object>} The coins count JSON (includes records of coin usage)
|
|
1196
|
+
* @async
|
|
1197
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1198
|
+
*/
|
|
1199
|
+
async getCoinsCount(){
|
|
1200
|
+
const url = new URL(this.params.whost + '/rest/1.0/inte/system/getrecord');
|
|
1201
|
+
|
|
1202
|
+
try{
|
|
1203
|
+
const req = await request(url, {
|
|
1204
|
+
headers: {
|
|
1205
|
+
'User-Agent': this.params.ua,
|
|
1206
|
+
'Cookie': this.params.cookie,
|
|
1207
|
+
},
|
|
1208
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
if (req.statusCode !== 200) {
|
|
1212
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const rdata = await req.body.json();
|
|
1216
|
+
return rdata;
|
|
1217
|
+
}
|
|
1218
|
+
catch (error) {
|
|
1219
|
+
throw new Error('getCoinsCount', { cause: error });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Retrieves the contents of a remote directory
|
|
1225
|
+
* @param {string} remoteDir - Remote directory path to list
|
|
1226
|
+
* @param {number} [page=1] - Page number for pagination
|
|
1227
|
+
* @returns {Promise<Object>} The directory listing JSON (includes entries array)
|
|
1228
|
+
* @async
|
|
1229
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1230
|
+
*/
|
|
1231
|
+
async getRemoteDir(remoteDir, page = 1){
|
|
1232
|
+
const url = new URL(this.params.whost + '/api/list');
|
|
1233
|
+
|
|
1234
|
+
try{
|
|
1235
|
+
const formData = new this.FormUrlEncoded();
|
|
1236
|
+
formData.append('order', 'name');
|
|
1237
|
+
formData.append('desc', 0);
|
|
1238
|
+
formData.append('dir', remoteDir);
|
|
1239
|
+
formData.append('num', 20000);
|
|
1240
|
+
formData.append('page', page);
|
|
1241
|
+
formData.append('showempty', 0);
|
|
1242
|
+
|
|
1243
|
+
const req = await request(url, {
|
|
1244
|
+
method: 'POST',
|
|
1245
|
+
body: formData.str(),
|
|
1246
|
+
headers: {
|
|
1247
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1248
|
+
'User-Agent': this.params.ua,
|
|
1249
|
+
'Cookie': this.params.cookie,
|
|
1250
|
+
},
|
|
1251
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
if (req.statusCode !== 200) {
|
|
1255
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const rdata = await req.body.json();
|
|
1259
|
+
return rdata;
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
throw new Error('getRemoteDir', { cause: error });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Retrieves the contents of a remote directory with specific file category
|
|
1268
|
+
* @param {number} [categoryId=1] - selected category:
|
|
1269
|
+
* <br>1: video
|
|
1270
|
+
* <br>2: audio
|
|
1271
|
+
* <br>3: pictures
|
|
1272
|
+
* <br>4: documents
|
|
1273
|
+
* <br>5: apps
|
|
1274
|
+
* <br>6: other
|
|
1275
|
+
* <br>7: torrent
|
|
1276
|
+
* @param {string} remoteDir - Remote directory path to list
|
|
1277
|
+
* @param {number} [page=1] - Page number for pagination
|
|
1278
|
+
* @returns {Promise<Object>} The directory listing JSON (includes entries array)
|
|
1279
|
+
* @async
|
|
1280
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1281
|
+
*/
|
|
1282
|
+
async getCategoryList(categoryId = 1, remoteDir = '/', page = 1, order = 'name', desc = 0, num = 20000){
|
|
1283
|
+
const url = new URL(this.params.whost + '/api/categorylist');
|
|
1284
|
+
|
|
1285
|
+
try{
|
|
1286
|
+
const formData = new this.FormUrlEncoded();
|
|
1287
|
+
formData.append('order', order);
|
|
1288
|
+
formData.append('desc', desc);
|
|
1289
|
+
formData.append('dir', remoteDir);
|
|
1290
|
+
formData.append('num', num);
|
|
1291
|
+
formData.append('page', page);
|
|
1292
|
+
formData.append('category', categoryId);
|
|
1293
|
+
|
|
1294
|
+
const req = await request(url, {
|
|
1295
|
+
method: 'POST',
|
|
1296
|
+
body: formData.str(),
|
|
1297
|
+
headers: {
|
|
1298
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1299
|
+
'User-Agent': this.params.ua,
|
|
1300
|
+
'Cookie': this.params.cookie,
|
|
1301
|
+
},
|
|
1302
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
if (req.statusCode !== 200) {
|
|
1306
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const rdata = await req.body.json();
|
|
1310
|
+
return rdata;
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
throw new Error('getCategoryList', { cause: error });
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Retrieves the contents of the recycle bin
|
|
1319
|
+
* @returns {Promise<Object>} The recycle bin listing JSON (includes entries array)
|
|
1320
|
+
* @async
|
|
1321
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1322
|
+
*/
|
|
1323
|
+
async getRecycleBin(page = 1){
|
|
1324
|
+
const url = new URL(this.params.whost + '/api/recycle/list');
|
|
1325
|
+
|
|
1326
|
+
try{
|
|
1327
|
+
url.search = new URLSearchParams({
|
|
1328
|
+
// order: 'name',
|
|
1329
|
+
desc: 0,
|
|
1330
|
+
num: 20000,
|
|
1331
|
+
page: page,
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
const req = await request(url, {
|
|
1336
|
+
headers: {
|
|
1337
|
+
'User-Agent': this.params.ua,
|
|
1338
|
+
'Cookie': this.params.cookie,
|
|
1339
|
+
},
|
|
1340
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
if (req.statusCode !== 200) {
|
|
1344
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const rdata = await req.body.json();
|
|
1348
|
+
return rdata;
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
throw new Error('getRecycleBin', { cause: error });
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Clears all items in the recycle bin
|
|
1357
|
+
* @returns {Promise<Object>} The clear recycle bin response JSON
|
|
1358
|
+
* @async
|
|
1359
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1360
|
+
*/
|
|
1361
|
+
async clearRecycleBin(){
|
|
1362
|
+
const url = new URL(this.params.whost + '/api/recycle/clear');
|
|
1363
|
+
|
|
1364
|
+
try{
|
|
1365
|
+
url.search = new URLSearchParams({
|
|
1366
|
+
'async': 1,
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
const req = await request(url, {
|
|
1370
|
+
headers: {
|
|
1371
|
+
'User-Agent': this.params.ua,
|
|
1372
|
+
'Cookie': this.params.cookie,
|
|
1373
|
+
},
|
|
1374
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
if (req.statusCode !== 200) {
|
|
1378
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const rdata = await req.body.json();
|
|
1382
|
+
return rdata;
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
throw new Error('clearRecycleBin', { cause: error });
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Initiates a precreate request for a file (reserve upload ID and pre-upload checks)
|
|
1391
|
+
* @param {Object} data - File data including remote_dir, file, size, upload_id (optional), and hash info
|
|
1392
|
+
* @param {string} data.remote_dir - Remote directory path
|
|
1393
|
+
* @param {string} data.file - Filename
|
|
1394
|
+
* @param {number} data.size - File size in bytes
|
|
1395
|
+
* @param {string} [data.upload_id] - Existing upload ID for resuming
|
|
1396
|
+
* @param {Object} data.hash - Hash information
|
|
1397
|
+
* @param {string} data.hash.file - MD5 hash of full file
|
|
1398
|
+
* @param {string} data.hash.slice - MD5 hash of first slice
|
|
1399
|
+
* @param {number} data.hash.crc32 - CRC32 value
|
|
1400
|
+
* @param {Array<string>} data.hash.chunks - Array of MD5 chunk hashes
|
|
1401
|
+
* @returns {Promise<Object>} The precreate response JSON (includes upload_id, etc.)
|
|
1402
|
+
* @async
|
|
1403
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1404
|
+
*/
|
|
1405
|
+
async precreateFile(data){
|
|
1406
|
+
const formData = new this.FormUrlEncoded();
|
|
1407
|
+
formData.append('path', makeRemoteFPath(data.remote_dir, data.file));
|
|
1408
|
+
// formData.append('target_path', data.remote_dir);
|
|
1409
|
+
formData.append('autoinit', 1);
|
|
1410
|
+
formData.append('size', data.size);
|
|
1411
|
+
formData.append('file_limit_switch_v34', 'true');
|
|
1412
|
+
formData.append('block_list', '[]');
|
|
1413
|
+
formData.append('rtype', 2);
|
|
1414
|
+
|
|
1415
|
+
if(data.upload_id && typeof data.upload_id === 'string' && data.upload_id !== ''){
|
|
1416
|
+
formData.append('uploadid', data.upload_id);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// check if has correct md5 values
|
|
1420
|
+
if(this.CheckMd5Val(data.hash.slice) && this.CheckMd5Val(data.hash.file)){
|
|
1421
|
+
formData.append('content-md5', data.hash.file);
|
|
1422
|
+
formData.append('slice-md5', data.hash.slice);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// check crc32int and ignore field for crc32 out of range
|
|
1426
|
+
if(Number.isSafeInteger(data.hash.crc32) && data.hash.crc32 >= 0 && data.hash.crc32 <= 0xFFFFFFFF){
|
|
1427
|
+
formData.append('content-crc32', data.hash.crc32);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// check chunks hash
|
|
1431
|
+
if(!this.CheckMd5Arr(data.hash.chunks)){
|
|
1432
|
+
const predefinedHash = ['5910a591dd8fc18c32a8f3df4fdc1761']
|
|
1433
|
+
|
|
1434
|
+
if(data.size > 4 * 1024 * 1024){
|
|
1435
|
+
predefinedHash.push('a5fc157d78e6ad1c7e114b056c92821e');
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
formData.set('block_list', JSON.stringify(predefinedHash));
|
|
1439
|
+
}
|
|
1440
|
+
else{
|
|
1441
|
+
formData.set('block_list', JSON.stringify(data.hash.chunks));
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// formData.append('local_ctime', '');
|
|
1445
|
+
// formData.append('local_mtime', '');
|
|
1446
|
+
|
|
1447
|
+
const url = new URL(this.params.whost + `/api/precreate`);
|
|
1448
|
+
|
|
1449
|
+
try{
|
|
1450
|
+
if(this.data.jsToken === ''){
|
|
1451
|
+
await this.updateAppData();
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
url.search = new URLSearchParams({
|
|
1455
|
+
...this.params.app,
|
|
1456
|
+
jsToken: this.data.jsToken,
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
const req = await request(url, {
|
|
1460
|
+
method: 'POST',
|
|
1461
|
+
body: formData.str(),
|
|
1462
|
+
headers: {
|
|
1463
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1464
|
+
'User-Agent': this.params.ua,
|
|
1465
|
+
'Cookie': this.params.cookie,
|
|
1466
|
+
},
|
|
1467
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
if (req.statusCode !== 200) {
|
|
1471
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// uploadid = 'P1-' + BASE64(ServerLocalIP + ':' + ServerTime + ':' + RequestID)
|
|
1475
|
+
const rdata = await req.body.json();
|
|
1476
|
+
// rdata.errno: 4000023 - need verify
|
|
1477
|
+
if(rdata.errno === 4000023){
|
|
1478
|
+
await this.updateAppData();
|
|
1479
|
+
return await this.precreateFile(data);
|
|
1480
|
+
}
|
|
1481
|
+
return rdata;
|
|
1482
|
+
}
|
|
1483
|
+
catch (error) {
|
|
1484
|
+
throw new Error('precreateFile', { cause: error });
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Attempts a rapid upload using existing file hashes (skip actual upload if file already on server)
|
|
1490
|
+
* @param {Object} data - File data including remote_dir, file, size, and hash info
|
|
1491
|
+
* @param {string} data.remote_dir - Remote directory path
|
|
1492
|
+
* @param {string} data.file - Filename
|
|
1493
|
+
* @param {number} data.size - File size in bytes
|
|
1494
|
+
* @param {Object} data.hash - Hash information
|
|
1495
|
+
* @param {string} data.hash.file - MD5 hash of full file
|
|
1496
|
+
* @param {string} data.hash.slice - MD5 hash of first slice
|
|
1497
|
+
* @param {number} data.hash.crc32 - CRC32 value
|
|
1498
|
+
* @param {Array<string>} [data.hash.chunks] - Array of MD5 chunk hashes
|
|
1499
|
+
* @returns {Promise<Object>} The rapid upload response JSON (indicates success or fallback)
|
|
1500
|
+
* @async
|
|
1501
|
+
* @throws {Error} Throws error if file size < 256KB, invalid hashes, HTTP status is not 200, or request fails
|
|
1502
|
+
*/
|
|
1503
|
+
async rapidUpload(data){
|
|
1504
|
+
const formData = new this.FormUrlEncoded({
|
|
1505
|
+
path: makeRemoteFPath(data.remote_dir, data.file),
|
|
1506
|
+
//target_path: data.remote_dir
|
|
1507
|
+
'content-length': data.size,
|
|
1508
|
+
'content-md5': data.hash.file,
|
|
1509
|
+
'slice-md5': data.hash.slice,
|
|
1510
|
+
'content-crc32': data.hash.crc32,
|
|
1511
|
+
//local_ctime: '',
|
|
1512
|
+
//local_mtime: '',
|
|
1513
|
+
block_list: JSON.stringify(data.hash.chunks || []),
|
|
1514
|
+
rtype: 2,
|
|
1515
|
+
mode: 1,
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
if(!this.CheckMd5Val(data.hash.slice) || !this.CheckMd5Val(data.hash.file)){
|
|
1519
|
+
const badMD5 = new Error('Bad MD5 Slice Hash or MD5 File Hash');
|
|
1520
|
+
throw new Error('rapidUpload', { cause: badMD5 });
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
if(!Number.isSafeInteger(data.hash.crc32) || data.hash.crc32 < 0 || data.hash.crc32 > 0xFFFFFFFF){
|
|
1524
|
+
formData.delete('content-crc32');
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if(!this.CheckMd5Arr(data.hash.chunks)){
|
|
1528
|
+
// use unsafe rapid upload if we don't have chunks hash
|
|
1529
|
+
formData.delete('block_list');
|
|
1530
|
+
formData.set('rtype', 3);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const url = new URL(this.params.whost + '/api/rapidupload');
|
|
1534
|
+
|
|
1535
|
+
try{
|
|
1536
|
+
if(data.size < 256 * 1024){
|
|
1537
|
+
throw new Error(`File size too small!`);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const req = await request(url, {
|
|
1541
|
+
method: 'POST',
|
|
1542
|
+
body: formData.str(),
|
|
1543
|
+
headers: {
|
|
1544
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1545
|
+
'User-Agent': this.params.ua,
|
|
1546
|
+
'Cookie': this.params.cookie,
|
|
1547
|
+
},
|
|
1548
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1549
|
+
});
|
|
1550
|
+
|
|
1551
|
+
if (req.statusCode !== 200) {
|
|
1552
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const rdata = await req.body.json();
|
|
1556
|
+
// rdata.errno: 2 - already exist?
|
|
1557
|
+
return rdata;
|
|
1558
|
+
}
|
|
1559
|
+
catch (error) {
|
|
1560
|
+
throw new Error('rapidUpload', { cause: error });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Attempts a upload file from remote server
|
|
1566
|
+
* @param {string} urls - Source urls (coma-separated)
|
|
1567
|
+
* @param {string} remote_dir - Remote directory path
|
|
1568
|
+
* @returns {Promise<Object>} The remote upload response JSON (indicates success or fallback)
|
|
1569
|
+
* @async
|
|
1570
|
+
* @throws {Error} Throws error if HTTP status is not 200, or request fails
|
|
1571
|
+
*/
|
|
1572
|
+
async remoteUpload(urls, remote_dir = '/Remote Upload'){
|
|
1573
|
+
const formData = new this.FormUrlEncoded({
|
|
1574
|
+
urls: urls,
|
|
1575
|
+
upload_to: remote_dir,
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
const url = new URL(this.params.whost + '/webmaster/remoteupload/submit');
|
|
1579
|
+
|
|
1580
|
+
try{
|
|
1581
|
+
const req = await request(url, {
|
|
1582
|
+
method: 'POST',
|
|
1583
|
+
body: formData.str(),
|
|
1584
|
+
headers: {
|
|
1585
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1586
|
+
'User-Agent': this.params.ua,
|
|
1587
|
+
'Cookie': this.params.cookie,
|
|
1588
|
+
},
|
|
1589
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
if (req.statusCode !== 200) {
|
|
1593
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
const rdata = await req.body.json();
|
|
1597
|
+
return rdata;
|
|
1598
|
+
}
|
|
1599
|
+
catch (error) {
|
|
1600
|
+
throw new Error('remoteUpload', { cause: error });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
/**
|
|
1605
|
+
* Retrieves an upload host endpoint to use for file uploads
|
|
1606
|
+
* @returns {Promise<Object>} The upload host response JSON (includes host field)
|
|
1607
|
+
* @async
|
|
1608
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1609
|
+
*/
|
|
1610
|
+
async getUploadHost(){
|
|
1611
|
+
const url = new URL(this.params.whost + '/rest/2.0/pcs/file?method=locateupload');
|
|
1612
|
+
try{
|
|
1613
|
+
const req = await request(url, {
|
|
1614
|
+
headers: {
|
|
1615
|
+
'User-Agent': this.params.ua,
|
|
1616
|
+
'Cookie': this.params.cookie,
|
|
1617
|
+
},
|
|
1618
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
if (req.statusCode !== 200) {
|
|
1622
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const rdata = await req.body.json();
|
|
1626
|
+
this.params.uhost = 'https://' + rdata.host;
|
|
1627
|
+
return rdata;
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
throw new Error('getUploadHost', { cause: error });
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* Uploads a single chunk (part) of a file
|
|
1636
|
+
* @param {Object} data - File data including remote_dir, file, upload_id
|
|
1637
|
+
* @param {number} partseq - The sequence number of this chunk (0-based)
|
|
1638
|
+
* @param {Blob|Buffer} blob - The binary data of the chunk
|
|
1639
|
+
* @param {function} [reqHandler] - Optional request progress handler
|
|
1640
|
+
* @param {AbortSignal} [externalAbort] - Optional external abort signal
|
|
1641
|
+
* @returns {Promise<Object>} The upload chunk response JSON (includes MD5 for chunk)
|
|
1642
|
+
* @async
|
|
1643
|
+
* @throws {Error} Throws error if HTTP status is not 200, chunk upload fails, or request times out
|
|
1644
|
+
*/
|
|
1645
|
+
async uploadChunk(data, partseq, blob, reqHandler, externalAbort) {
|
|
1646
|
+
const timeoutAborter = new AbortController;
|
|
1647
|
+
const timeoutId = setTimeout(() => { timeoutAborter.abort(); }, this.TERABOX_TIMEOUT);
|
|
1648
|
+
externalAbort = externalAbort ? externalAbort : new AbortController().signal;
|
|
1649
|
+
|
|
1650
|
+
const url = new URL(`${this.params.uhost}/rest/2.0/pcs/superfile2`);
|
|
1651
|
+
url.search = new URLSearchParams({
|
|
1652
|
+
method: 'upload',
|
|
1653
|
+
...this.params.app,
|
|
1654
|
+
// type: 'tmpfile',
|
|
1655
|
+
path: makeRemoteFPath(data.remote_dir, data.file),
|
|
1656
|
+
uploadid: data.upload_id,
|
|
1657
|
+
// uploadsign: 0,
|
|
1658
|
+
partseq: partseq,
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
const formData = new FormData();
|
|
1662
|
+
formData.append('file', blob, 'blob');
|
|
1663
|
+
|
|
1664
|
+
const req = await request(url, {
|
|
1665
|
+
method: 'POST',
|
|
1666
|
+
body: formData,
|
|
1667
|
+
headers: {
|
|
1668
|
+
'User-Agent': this.params.ua,
|
|
1669
|
+
'Cookie': this.params.cookie,
|
|
1670
|
+
},
|
|
1671
|
+
signal: AbortSignal.any([
|
|
1672
|
+
externalAbort,
|
|
1673
|
+
timeoutAborter.signal,
|
|
1674
|
+
]),
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
clearTimeout(timeoutId);
|
|
1678
|
+
|
|
1679
|
+
if (req.statusCode !== 200) {
|
|
1680
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const res = await req.body.json();
|
|
1684
|
+
if (res.error_code) {
|
|
1685
|
+
const uploadError = new Error(`Upload failed! Error Code #${res.error_code}`);
|
|
1686
|
+
uploadError.data = res;
|
|
1687
|
+
throw uploadError;
|
|
1688
|
+
}
|
|
1689
|
+
return res;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* Creates a new directory in the remote file system
|
|
1694
|
+
* @param {string} remoteDir - The path of the directory to create
|
|
1695
|
+
* @returns {Promise<Object>} The create directory response JSON
|
|
1696
|
+
* @async
|
|
1697
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1698
|
+
*/
|
|
1699
|
+
async createDir(remoteDir){
|
|
1700
|
+
const formData = new this.FormUrlEncoded();
|
|
1701
|
+
formData.append('path', remoteDir);
|
|
1702
|
+
formData.append('isdir', 1);
|
|
1703
|
+
formData.append('block_list', '[]');
|
|
1704
|
+
|
|
1705
|
+
const url = new URL(this.params.whost + '/api/create?a=commit');
|
|
1706
|
+
|
|
1707
|
+
try{
|
|
1708
|
+
const req = await request(url, {
|
|
1709
|
+
method: 'POST',
|
|
1710
|
+
body: formData.str(),
|
|
1711
|
+
headers: {
|
|
1712
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1713
|
+
'User-Agent': this.params.ua,
|
|
1714
|
+
'Cookie': this.params.cookie,
|
|
1715
|
+
},
|
|
1716
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
if (req.statusCode !== 200) {
|
|
1720
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const rdata = await req.body.json();
|
|
1724
|
+
// rdata.errno: -7 - param path file name is invalid
|
|
1725
|
+
return rdata;
|
|
1726
|
+
}
|
|
1727
|
+
catch (error) {
|
|
1728
|
+
throw new Error('createFolder', { cause: error });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Creates a new file entry on the server after uploading chunks
|
|
1734
|
+
* @param {Object} data - File data including remote_dir, file, size, hash, upload_id, and chunks
|
|
1735
|
+
* @param {string} data.remote_dir - Remote directory path
|
|
1736
|
+
* @param {string} data.file - Filename
|
|
1737
|
+
* @param {number} data.size - File size in bytes
|
|
1738
|
+
* @param {Object} data.hash - Hash information
|
|
1739
|
+
* @param {string} data.hash.file - MD5 hash of full file
|
|
1740
|
+
* @param {string} data.hash.slice - MD5 hash of first slice
|
|
1741
|
+
* @param {number} data.hash.crc32 - CRC32 value
|
|
1742
|
+
* @param {Array<string>} data.hash.chunks - Array of MD5 chunk hashes
|
|
1743
|
+
* @param {string} data.upload_id - Upload ID obtained from precreate
|
|
1744
|
+
* @returns {Promise<Object>} The create file response JSON (includes MD5 and ETag)
|
|
1745
|
+
* @async
|
|
1746
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1747
|
+
*/
|
|
1748
|
+
async createFile(data){
|
|
1749
|
+
const formData = new this.FormUrlEncoded();
|
|
1750
|
+
formData.append('path', makeRemoteFPath(data.remote_dir, data.file));
|
|
1751
|
+
// formData.append('isdir', 0);
|
|
1752
|
+
formData.append('size', data.size);
|
|
1753
|
+
formData.append('isdir', 0);
|
|
1754
|
+
|
|
1755
|
+
// check if has correct md5 values
|
|
1756
|
+
if(this.CheckMd5Val(data.hash.slice) && this.CheckMd5Val(data.hash.file)){
|
|
1757
|
+
formData.append('content-md5', data.hash.file);
|
|
1758
|
+
formData.append('slice-md5', data.hash.slice);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// check crc32int and ignore field for crc32 out of range
|
|
1762
|
+
if(Number.isSafeInteger(data.hash.crc32) && data.hash.crc32 >= 0 && data.hash.crc32 <= 0xFFFFFFFF){
|
|
1763
|
+
formData.append('content-crc32', data.hash.crc32);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
formData.append('block_list', JSON.stringify(data.hash.chunks));;
|
|
1767
|
+
formData.append('uploadid', data.upload_id);
|
|
1768
|
+
formData.append('rtype', 2);
|
|
1769
|
+
|
|
1770
|
+
// formData.append('local_ctime', '');
|
|
1771
|
+
// formData.append('local_mtime', '');
|
|
1772
|
+
// formData.append('zip_quality', '');
|
|
1773
|
+
// formData.append('zip_sign', '');
|
|
1774
|
+
// formData.append('is_revision', 0);
|
|
1775
|
+
// formData.append('mode', 2); // 2 is Batch Upload
|
|
1776
|
+
// formData.append('exif_info', exifJsonStr);
|
|
1777
|
+
|
|
1778
|
+
const url = new URL(this.params.whost + `/api/create`);
|
|
1779
|
+
|
|
1780
|
+
try{
|
|
1781
|
+
const req = await request(url, {
|
|
1782
|
+
method: 'POST',
|
|
1783
|
+
body: formData.str(),
|
|
1784
|
+
headers: {
|
|
1785
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1786
|
+
'User-Agent': this.params.ua,
|
|
1787
|
+
'Cookie': this.params.cookie,
|
|
1788
|
+
},
|
|
1789
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1790
|
+
});
|
|
1791
|
+
|
|
1792
|
+
if (req.statusCode !== 200) {
|
|
1793
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
const rdata = await req.body.json();
|
|
1797
|
+
// rdata.errno: 31355 - pcs service failed
|
|
1798
|
+
if(rdata.md5){
|
|
1799
|
+
// encrypted etag
|
|
1800
|
+
rdata.emd5 = rdata.md5;
|
|
1801
|
+
// decrypted etag (without chunk count)
|
|
1802
|
+
rdata.md5 = this.DecodeMd5(rdata.emd5);
|
|
1803
|
+
// set custom etag
|
|
1804
|
+
rdata.etag = rdata.md5;
|
|
1805
|
+
if(data.hash.chunks.length > 1){
|
|
1806
|
+
rdata.etag += '-' + data.hash.chunks.length;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
return rdata;
|
|
1810
|
+
}
|
|
1811
|
+
catch (error) {
|
|
1812
|
+
console.log(error);
|
|
1813
|
+
throw new Error('createFile', { cause: error });
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Performs file management operations (delete, copy, move, rename)
|
|
1819
|
+
* @param {string} operation - Operation type: 'delete', 'copy', 'move', 'rename'
|
|
1820
|
+
* @param {Array} fmparams - Parameters for the operation (array of paths or objects)
|
|
1821
|
+
* @returns {Promise<Object>} The file manager response JSON
|
|
1822
|
+
* @async
|
|
1823
|
+
* @throws {Error} Throws error if fmparams is not an array, HTTP status is not 200, or request fails
|
|
1824
|
+
*/
|
|
1825
|
+
async filemanager(operation, fmparams){
|
|
1826
|
+
// For Delete: ["/path1","path2.rar"]
|
|
1827
|
+
// For Move: [{"path":"/myfolder/source.bin","dest":"/target/","newname":"newfilename.bin"}]
|
|
1828
|
+
// For Copy same as move
|
|
1829
|
+
// + "ondup": newcopy, overwrite (optional, skip by default)
|
|
1830
|
+
// For rename [{"id":1111,"path":"/dir1/src.bin","newname":"myfile2.bin"}]
|
|
1831
|
+
|
|
1832
|
+
// operation - copy (file copy), move (file movement), rename (file renaming), and delete (file deletion)
|
|
1833
|
+
// opera=copy: filelist: [{"path":"/hello/test.mp4","dest":"","newname":"test.mp4"}]
|
|
1834
|
+
// opera=move: filelist: [{"path":"/test.mp4","dest":"/test_dir","newname":"test.mp4"}]
|
|
1835
|
+
// opera=rename: filelist:[{"path":"/hello/test.mp4","newname":"test_one.mp4"}]
|
|
1836
|
+
// opera=delete: filelist: ["/test.mp4"]
|
|
1837
|
+
|
|
1838
|
+
if(!Array.isArray(fmparams)){
|
|
1839
|
+
throw new Error('filemanager', { cause: new Error('FS paths should be in array!') });
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const url = new URL(this.params.whost + '/api/filemanager');
|
|
1843
|
+
|
|
1844
|
+
const formData = new this.FormUrlEncoded();
|
|
1845
|
+
formData.append('filelist', JSON.stringify(fmparams));
|
|
1846
|
+
|
|
1847
|
+
try{
|
|
1848
|
+
if(this.data.jsToken === ''){
|
|
1849
|
+
await this.updateAppData();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
url.search = new URLSearchParams({
|
|
1853
|
+
...this.params.app,
|
|
1854
|
+
jsToken: this.data.jsToken,
|
|
1855
|
+
// 'async': 1,
|
|
1856
|
+
onnest: 'fail',
|
|
1857
|
+
opera: operation, // delete, copy, move, rename
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
const req = await request(url, {
|
|
1861
|
+
method: 'POST',
|
|
1862
|
+
body: formData.str(),
|
|
1863
|
+
headers: {
|
|
1864
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1865
|
+
'User-Agent': this.params.ua,
|
|
1866
|
+
'Cookie': this.params.cookie,
|
|
1867
|
+
},
|
|
1868
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1869
|
+
});
|
|
1870
|
+
|
|
1871
|
+
if (req.statusCode !== 200) {
|
|
1872
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const rdata = await req.body.json();
|
|
1876
|
+
if(rdata.errno === 450016){
|
|
1877
|
+
await this.updateAppData();
|
|
1878
|
+
return await this.filemanager(operation, fmparams);
|
|
1879
|
+
}
|
|
1880
|
+
return rdata;
|
|
1881
|
+
}
|
|
1882
|
+
catch (error) {
|
|
1883
|
+
throw new Error('filemanager', { cause: error });
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
/**
|
|
1888
|
+
* Retrieves a list of shares created by the user
|
|
1889
|
+
* @returns {Promise<Object>} The share list JSON (includes share entries)
|
|
1890
|
+
* @async
|
|
1891
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1892
|
+
*/
|
|
1893
|
+
async shareList(page = 1){
|
|
1894
|
+
const url = new URL(this.params.whost + '/share/teratransfer/sharelist');
|
|
1895
|
+
|
|
1896
|
+
try{
|
|
1897
|
+
url.search = new URLSearchParams({
|
|
1898
|
+
// ...this.params.app,
|
|
1899
|
+
page_size: 100,
|
|
1900
|
+
page: page,
|
|
1901
|
+
});
|
|
1902
|
+
|
|
1903
|
+
const req = await request(url, {
|
|
1904
|
+
headers: {
|
|
1905
|
+
'User-Agent': this.params.ua,
|
|
1906
|
+
'Cookie': this.params.cookie,
|
|
1907
|
+
},
|
|
1908
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1909
|
+
});
|
|
1910
|
+
|
|
1911
|
+
const rdata = await req.body.json();
|
|
1912
|
+
return rdata;
|
|
1913
|
+
}
|
|
1914
|
+
catch (error) {
|
|
1915
|
+
throw new Error('shareList', { cause: error });
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Sets sharing parameters (e.g., password, expiration) for specified files
|
|
1921
|
+
* @param {Array<string>} filelist - Array of file paths to share
|
|
1922
|
+
* @param {string} [pass=''] - Optional 4-character alphanumeric password
|
|
1923
|
+
* @param {number} [period=0] - Sharing period in days (0 for no expiration)
|
|
1924
|
+
* @returns {Promise<Object>} The share set response JSON (includes share IDs)
|
|
1925
|
+
* @async
|
|
1926
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1927
|
+
*/
|
|
1928
|
+
async shareSet(filelist, pass = '', period = 0){
|
|
1929
|
+
const url = new URL(this.params.whost + '/share/pset');
|
|
1930
|
+
|
|
1931
|
+
try{
|
|
1932
|
+
url.search = new URLSearchParams({
|
|
1933
|
+
// ...this.params.app,
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
filelist = Array.isArray(filelist) ? filelist : [];
|
|
1937
|
+
filelist = JSON.stringify(filelist);
|
|
1938
|
+
|
|
1939
|
+
pass = typeof pass === 'string' && pass.match(/^[0-9a-z]{4}$/i) ? pass : '';
|
|
1940
|
+
const schannel = pass !== '' ? 4 : 0;
|
|
1941
|
+
|
|
1942
|
+
// 0 - infinity, otherwise valid X days
|
|
1943
|
+
period = parseInt(period);
|
|
1944
|
+
period = !isNaN(period) && Number.isSafeInteger(period) ? period : 0;
|
|
1945
|
+
|
|
1946
|
+
const formData = new this.FormUrlEncoded();
|
|
1947
|
+
formData.append('schannel', schannel);
|
|
1948
|
+
formData.append('channel_list', '[]');
|
|
1949
|
+
formData.append('period', period);
|
|
1950
|
+
formData.append('path_list', filelist);
|
|
1951
|
+
formData.append('pwd', pass);
|
|
1952
|
+
|
|
1953
|
+
const req = await request(url, {
|
|
1954
|
+
headers: {
|
|
1955
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1956
|
+
'User-Agent': this.params.ua,
|
|
1957
|
+
'Cookie': this.params.cookie,
|
|
1958
|
+
Referer: this.params.whost,
|
|
1959
|
+
},
|
|
1960
|
+
body: formData.str(),
|
|
1961
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
const rdata = await req.body.json();
|
|
1965
|
+
return rdata;
|
|
1966
|
+
}
|
|
1967
|
+
catch (error) {
|
|
1968
|
+
throw new Error('shareSet', { cause: error });
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
/**
|
|
1973
|
+
* Cancels existing shares by share ID
|
|
1974
|
+
* @param {Array<number>} [shareid_list=[]] - Array of share IDs to cancel
|
|
1975
|
+
* @returns {Promise<Object>} The share cancel response JSON
|
|
1976
|
+
* @async
|
|
1977
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
1978
|
+
*/
|
|
1979
|
+
async shareCancel(shareid_list = []){
|
|
1980
|
+
const url = new URL(this.params.whost + '/share/cancel');
|
|
1981
|
+
|
|
1982
|
+
try{
|
|
1983
|
+
url.search = new URLSearchParams({
|
|
1984
|
+
// ...this.params.app,
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
shareid_list = Array.isArray(shareid_list) ? shareid_list : [];
|
|
1988
|
+
shareid_list = JSON.stringify(shareid_list);
|
|
1989
|
+
|
|
1990
|
+
const formData = new this.FormUrlEncoded();
|
|
1991
|
+
formData.append('shareid_list', shareid_list);
|
|
1992
|
+
|
|
1993
|
+
const req = await request(url, {
|
|
1994
|
+
headers: {
|
|
1995
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1996
|
+
'User-Agent': this.params.ua,
|
|
1997
|
+
'Cookie': this.params.cookie,
|
|
1998
|
+
Referer: this.params.whost,
|
|
1999
|
+
},
|
|
2000
|
+
body: formData.str(),
|
|
2001
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2002
|
+
});
|
|
2003
|
+
|
|
2004
|
+
const rdata = await req.body.json();
|
|
2005
|
+
return rdata;
|
|
2006
|
+
}
|
|
2007
|
+
catch (error) {
|
|
2008
|
+
throw new Error('shareCancel', { cause: error });
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Retrieves information for a shortened URL share
|
|
2014
|
+
* @param {string} shortUrl - The short url: after "surl="
|
|
2015
|
+
* @returns {Promise<Object>} The short URL info JSON (includes file list, permissions)
|
|
2016
|
+
* @async
|
|
2017
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2018
|
+
*/
|
|
2019
|
+
async shortUrlInfo(shortUrl){
|
|
2020
|
+
const url = new URL(this.params.whost + '/api/shorturlinfo');
|
|
2021
|
+
|
|
2022
|
+
try{
|
|
2023
|
+
url.search = new URLSearchParams({
|
|
2024
|
+
//...this.params.app,
|
|
2025
|
+
shorturl: '1' + shortUrl,
|
|
2026
|
+
root: 1,
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
const connector = buildConnector({ ciphers: tls.DEFAULT_CIPHERS + ':!ECDHE-RSA-AES128-SHA' });
|
|
2030
|
+
const client = new Client(this.params.whost, { connect: connector });
|
|
2031
|
+
const req = await request(url, {
|
|
2032
|
+
method: 'GET',
|
|
2033
|
+
headers: {
|
|
2034
|
+
'User-Agent': this.params.ua,
|
|
2035
|
+
'Cookie': this.params.cookie,
|
|
2036
|
+
},
|
|
2037
|
+
dispatcher: client,
|
|
2038
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2039
|
+
});
|
|
2040
|
+
|
|
2041
|
+
if (req.statusCode !== 200) {
|
|
2042
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const rdata = await req.body.json();
|
|
2046
|
+
return rdata;
|
|
2047
|
+
}
|
|
2048
|
+
catch (error) {
|
|
2049
|
+
throw new Error('shortUrlInfo', { cause: error });
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* Lists files under a shortened URL share
|
|
2055
|
+
* @param {string} shortUrl - The short url: after "surl="
|
|
2056
|
+
* @param {string} [remoteDir=''] - Remote directory under share (empty for root)
|
|
2057
|
+
* @param {number} [page=1] - Page number for pagination
|
|
2058
|
+
* @returns {Promise<Object>} The short URL file list JSON (includes entries array)
|
|
2059
|
+
* @async
|
|
2060
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2061
|
+
*/
|
|
2062
|
+
async shortUrlList(shortUrl, remoteDir = '', page = 1){
|
|
2063
|
+
const url = new URL(this.params.whost + '/share/list');
|
|
2064
|
+
remoteDir = remoteDir || ''
|
|
2065
|
+
|
|
2066
|
+
try{
|
|
2067
|
+
if(this.data.jsToken === ''){
|
|
2068
|
+
await this.updateAppData();
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
url.search = new URLSearchParams({
|
|
2072
|
+
...this.params.app,
|
|
2073
|
+
jsToken: this.data.jsToken,
|
|
2074
|
+
shorturl: shortUrl,
|
|
2075
|
+
by: 'name',
|
|
2076
|
+
order: 'asc',
|
|
2077
|
+
num: 20000,
|
|
2078
|
+
dir: remoteDir,
|
|
2079
|
+
page: page,
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
if(remoteDir === ''){
|
|
2083
|
+
url.searchParams.append('root', '1');
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const connector = buildConnector({ ciphers: tls.DEFAULT_CIPHERS + ':!ECDHE-RSA-AES128-SHA' });
|
|
2087
|
+
const client = new Client(this.params.whost, { connect: connector });
|
|
2088
|
+
const req = await request(url, {
|
|
2089
|
+
method: 'GET',
|
|
2090
|
+
headers: {
|
|
2091
|
+
'User-Agent': this.params.ua,
|
|
2092
|
+
'Cookie': this.params.cookie,
|
|
2093
|
+
},
|
|
2094
|
+
dispatcher: client,
|
|
2095
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2096
|
+
});
|
|
2097
|
+
|
|
2098
|
+
if (req.statusCode !== 200) {
|
|
2099
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
const rdata = await req.body.json();
|
|
2103
|
+
// rdata.errno: 4000020 - need verify
|
|
2104
|
+
if(rdata.errno === 4000020){
|
|
2105
|
+
await this.updateAppData();
|
|
2106
|
+
return await this.shortUrlList(shortUrl, remoteDir, page);
|
|
2107
|
+
}
|
|
2108
|
+
return rdata;
|
|
2109
|
+
}
|
|
2110
|
+
catch (error) {
|
|
2111
|
+
throw new Error('shortUrlList', { cause: error });
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
/**
|
|
2116
|
+
* Retrieves file difference (delta) information for synchronization
|
|
2117
|
+
* @returns {Promise<Object>} The file diff JSON (includes entries, request_id, has_more flag)
|
|
2118
|
+
* @async
|
|
2119
|
+
* @throws {Error} Throws error if HTTP status is not 200, request fails, or on recursive errors
|
|
2120
|
+
*/
|
|
2121
|
+
async fileDiff(){
|
|
2122
|
+
const formData = new this.FormUrlEncoded();
|
|
2123
|
+
formData.append('cursor', this.params.cursor);
|
|
2124
|
+
if(this.params.cursor === 'null'){
|
|
2125
|
+
formData.append('c', 'full');
|
|
2126
|
+
}
|
|
2127
|
+
formData.append('action', 'manual');
|
|
2128
|
+
|
|
2129
|
+
const url = new URL(this.params.whost + '/api/filediff');
|
|
2130
|
+
url.search = new URLSearchParams({
|
|
2131
|
+
...this.params.app,
|
|
2132
|
+
block_list: 1,
|
|
2133
|
+
// rand: '',
|
|
2134
|
+
// time: '',
|
|
2135
|
+
// vip: this.params.vip_type,
|
|
2136
|
+
// wp_retry_num: 2,
|
|
2137
|
+
// lang: this.params.lang,
|
|
2138
|
+
// logid: '',
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
try{
|
|
2142
|
+
const req = await request(url, {
|
|
2143
|
+
method: 'POST',
|
|
2144
|
+
body: formData.str(),
|
|
2145
|
+
headers: {
|
|
2146
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2147
|
+
'User-Agent': this.params.ua,
|
|
2148
|
+
'Cookie': this.params.cookie,
|
|
2149
|
+
},
|
|
2150
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
if (req.statusCode !== 200) {
|
|
2154
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const rdata = await req.body.json();
|
|
2158
|
+
if(rdata.errno === 0){
|
|
2159
|
+
this.params.cursor = rdata.cursor;
|
|
2160
|
+
if(!Array.isArray(rdata.request_id)){
|
|
2161
|
+
rdata.request_id = [ rdata.request_id ];
|
|
2162
|
+
}
|
|
2163
|
+
if(rdata.has_more){
|
|
2164
|
+
// Extra FileDiff request...
|
|
2165
|
+
const rFileDiff = await this.fileDiff();
|
|
2166
|
+
if(rFileDiff.errno === 0){
|
|
2167
|
+
rdata.reset = rFileDiff.reset;
|
|
2168
|
+
rdata.request_id = rdata.request_id.concat(rFileDiff.request_id);
|
|
2169
|
+
rdata.entries = Object.assign({}, rdata.entries, rFileDiff.entries);
|
|
2170
|
+
rdata.has_more = rFileDiff.has_more;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
return rdata;
|
|
2175
|
+
}
|
|
2176
|
+
catch (error) {
|
|
2177
|
+
this.params.cursor = 'null';
|
|
2178
|
+
throw new Error('fileDiff', { cause: error });
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Generates a PAN token for subsequent API requests
|
|
2184
|
+
* @returns {Promise<Object>} The PAN token response JSON (includes pan token and expire)
|
|
2185
|
+
* @async
|
|
2186
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2187
|
+
*/
|
|
2188
|
+
async genPanToken(){
|
|
2189
|
+
const url = new URL(this.params.whost + '/api/pantoken');
|
|
2190
|
+
|
|
2191
|
+
try{
|
|
2192
|
+
url.search = new URLSearchParams({
|
|
2193
|
+
...this.params.app,
|
|
2194
|
+
lang: this.params.lang,
|
|
2195
|
+
u: 'https://www.terabox.com',
|
|
2196
|
+
});
|
|
2197
|
+
|
|
2198
|
+
const req = await request(url, {
|
|
2199
|
+
headers: {
|
|
2200
|
+
'User-Agent': this.params.ua,
|
|
2201
|
+
'Cookie': this.params.cookie,
|
|
2202
|
+
},
|
|
2203
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2204
|
+
});
|
|
2205
|
+
|
|
2206
|
+
if (req.statusCode !== 200) {
|
|
2207
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
const rdata = await req.body.json();
|
|
2211
|
+
return rdata;
|
|
2212
|
+
}
|
|
2213
|
+
catch (error) {
|
|
2214
|
+
throw new Error('genPanToken', { cause: error });
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
/**
|
|
2219
|
+
* Retrieves home page information (user info, sign data)
|
|
2220
|
+
* @returns {Promise<Object>} The home info JSON (includes sign1, sign3, data.signb)
|
|
2221
|
+
* @async
|
|
2222
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2223
|
+
*/
|
|
2224
|
+
async getHomeInfo(){
|
|
2225
|
+
const url = new URL(this.params.whost + '/api/home/info');
|
|
2226
|
+
|
|
2227
|
+
try{
|
|
2228
|
+
const req = await request(url, {
|
|
2229
|
+
headers: {
|
|
2230
|
+
'User-Agent': this.params.ua,
|
|
2231
|
+
'Cookie': this.params.cookie,
|
|
2232
|
+
},
|
|
2233
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2234
|
+
});
|
|
2235
|
+
|
|
2236
|
+
if (req.statusCode !== 200) {
|
|
2237
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const rdata = await req.body.json();
|
|
2241
|
+
if(rdata.errno === 0){
|
|
2242
|
+
rdata.data.signb = this.SignDownload(rdata.data.sign3, rdata.data.sign1);
|
|
2243
|
+
}
|
|
2244
|
+
return rdata;
|
|
2245
|
+
}
|
|
2246
|
+
catch (error) {
|
|
2247
|
+
throw new Error('getHomeInfo', { cause: error });
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
/**
|
|
2252
|
+
* Initiates a download request for specified file IDs
|
|
2253
|
+
* @param {Array<number>} fs_ids - Array of file system IDs to download
|
|
2254
|
+
* @param {string} signb - Base64-encoded signature from getHomeInfo
|
|
2255
|
+
* @returns {Promise<Object>} The download response JSON (includes dlink URLs)
|
|
2256
|
+
* @async
|
|
2257
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2258
|
+
*/
|
|
2259
|
+
async download(fs_ids){
|
|
2260
|
+
const url = new URL(this.params.whost + '/api/download');
|
|
2261
|
+
|
|
2262
|
+
try{
|
|
2263
|
+
const homeInfo = await this.getHomeInfo();
|
|
2264
|
+
if(homeInfo.errno !== 0){
|
|
2265
|
+
throw new Error(`API error! Bad HomeInfo response`);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
const formData = new this.FormUrlEncoded({
|
|
2269
|
+
fidlist: JSON.stringify(fs_ids),
|
|
2270
|
+
type: 'dlink',
|
|
2271
|
+
vip: 2, // this.params.vip_type
|
|
2272
|
+
sign: homeInfo.data.signb,
|
|
2273
|
+
timestamp: homeInfo.data.timestamp,
|
|
2274
|
+
need_speed: 1, // Premium speed?..
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
const req = await request(url, {
|
|
2278
|
+
method: 'POST',
|
|
2279
|
+
body: formData.str(),
|
|
2280
|
+
headers: {
|
|
2281
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2282
|
+
'User-Agent': this.params.ua,
|
|
2283
|
+
'Cookie': this.params.cookie,
|
|
2284
|
+
},
|
|
2285
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
if (req.statusCode !== 200) {
|
|
2289
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
const rdata = await req.body.json();
|
|
2293
|
+
return rdata;
|
|
2294
|
+
}
|
|
2295
|
+
catch (error) {
|
|
2296
|
+
throw new Error('download', { cause: error });
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* Retrieves the streaming contents of a remote file
|
|
2302
|
+
* @param {string} remotePath - Remote video file
|
|
2303
|
+
* @param {string} type - Streaming type:
|
|
2304
|
+
* <br>M3U8_FLV_264_480
|
|
2305
|
+
* <br>M3U8_AUTO_240
|
|
2306
|
+
* <br>M3U8_AUTO_360
|
|
2307
|
+
* <br>M3U8_AUTO_480
|
|
2308
|
+
* <br>M3U8_AUTO_720
|
|
2309
|
+
* <br>M3U8_AUTO_1080
|
|
2310
|
+
* <br>M3U8_SUBTITLE_SRT
|
|
2311
|
+
* @returns {Promise<Object>} m3u8 playlist, or JSON with error
|
|
2312
|
+
* @async
|
|
2313
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2314
|
+
*/
|
|
2315
|
+
async getStream(remotePath = '/video.mp4', type = 'M3U8_AUTO_480'){
|
|
2316
|
+
const url = new URL(this.params.whost + '/api/streaming');
|
|
2317
|
+
|
|
2318
|
+
try{
|
|
2319
|
+
const formData = new this.FormUrlEncoded();
|
|
2320
|
+
formData.append('path', remotePath);
|
|
2321
|
+
formData.append('type', type);
|
|
2322
|
+
formData.append('vip', 2);
|
|
2323
|
+
|
|
2324
|
+
const req = await request(url, {
|
|
2325
|
+
method: 'POST',
|
|
2326
|
+
body: formData.str(),
|
|
2327
|
+
headers: {
|
|
2328
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2329
|
+
'User-Agent': this.params.ua,
|
|
2330
|
+
'Cookie': this.params.cookie,
|
|
2331
|
+
},
|
|
2332
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2333
|
+
});
|
|
2334
|
+
|
|
2335
|
+
if (req.statusCode !== 200) {
|
|
2336
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
const rdata = await req.body.json();
|
|
2340
|
+
return rdata;
|
|
2341
|
+
}
|
|
2342
|
+
catch (error) {
|
|
2343
|
+
throw new Error('getStream', { cause: error });
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* Retrieves metadata for specified remote files
|
|
2349
|
+
* @param {Array<Object>} remote_file_list - Array of file descriptor objects { fs_id, path, etc. }
|
|
2350
|
+
* @returns {Promise<Object>} The file metadata JSON (includes size, md5, etc.)
|
|
2351
|
+
* @async
|
|
2352
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2353
|
+
*/
|
|
2354
|
+
async getFileMeta(remote_file_list){
|
|
2355
|
+
const url = new URL(this.params.whost + '/api/filemetas');
|
|
2356
|
+
|
|
2357
|
+
try{
|
|
2358
|
+
const formData = new this.FormUrlEncoded({
|
|
2359
|
+
dlink: 1,
|
|
2360
|
+
origin: 'dlna',
|
|
2361
|
+
target: JSON.stringify(remote_file_list),
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
const req = await request(url, {
|
|
2365
|
+
method: 'POST',
|
|
2366
|
+
body: formData.str(),
|
|
2367
|
+
headers: {
|
|
2368
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2369
|
+
'User-Agent': this.params.ua,
|
|
2370
|
+
'Cookie': this.params.cookie,
|
|
2371
|
+
},
|
|
2372
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2373
|
+
});
|
|
2374
|
+
|
|
2375
|
+
if (req.statusCode !== 200) {
|
|
2376
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
const rdata = await req.body.json();
|
|
2380
|
+
return rdata;
|
|
2381
|
+
}
|
|
2382
|
+
catch (error) {
|
|
2383
|
+
throw new Error('getFileMeta', { cause: error });
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* Retrieves a list of recent uploads for the account
|
|
2389
|
+
* @param {number} [page=1] - Page number for pagination
|
|
2390
|
+
* @returns {Promise<Object>} The recent uploads JSON (includes records array)
|
|
2391
|
+
* @async
|
|
2392
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2393
|
+
*/
|
|
2394
|
+
async getRecentUploads(page = 1){
|
|
2395
|
+
const url = new URL(this.params.whost + '/rest/recent/listall');
|
|
2396
|
+
|
|
2397
|
+
try{
|
|
2398
|
+
url.search = new URLSearchParams({
|
|
2399
|
+
...this.params.app,
|
|
2400
|
+
version: this.params.ver_android,
|
|
2401
|
+
// num: 20000, ???
|
|
2402
|
+
// page: page, ???
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
const req = await request(url, {
|
|
2406
|
+
method: 'GET',
|
|
2407
|
+
body: formData.str(),
|
|
2408
|
+
headers: {
|
|
2409
|
+
'User-Agent': this.params.ua,
|
|
2410
|
+
'Cookie': this.params.cookie,
|
|
2411
|
+
},
|
|
2412
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2413
|
+
});
|
|
2414
|
+
|
|
2415
|
+
if (req.statusCode !== 200) {
|
|
2416
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
const rdata = await req.body.json();
|
|
2420
|
+
return rdata;
|
|
2421
|
+
}
|
|
2422
|
+
catch (error) {
|
|
2423
|
+
throw new Error('getRecentUploads', { cause: error });
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/**
|
|
2428
|
+
* Retrieves the RSA public key from the server for encryption
|
|
2429
|
+
* @returns {Promise<Object>} The public key response JSON (includes pp1 and pp2)
|
|
2430
|
+
* @async
|
|
2431
|
+
* @throws {Error} Throws error if HTTP status is not 200 or request fails
|
|
2432
|
+
*/
|
|
2433
|
+
async getPublicKey(){
|
|
2434
|
+
const url = new URL(this.params.whost + '/passport/getpubkey');
|
|
2435
|
+
|
|
2436
|
+
try{
|
|
2437
|
+
const req = await request(url, {
|
|
2438
|
+
method: 'GET',
|
|
2439
|
+
headers: {
|
|
2440
|
+
'User-Agent': this.params.ua,
|
|
2441
|
+
},
|
|
2442
|
+
signal: AbortSignal.timeout(this.TERABOX_TIMEOUT),
|
|
2443
|
+
});
|
|
2444
|
+
|
|
2445
|
+
if (req.statusCode !== 200) {
|
|
2446
|
+
throw new Error(`HTTP error! Status: ${req.statusCode}`);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
const rdata = await req.body.json();
|
|
2450
|
+
|
|
2451
|
+
if(rdata.code === 0){
|
|
2452
|
+
this.data.pubkey = this.DecryptAES(rdata.data.pp1, rdata.data.pp2);
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
return rdata;
|
|
2456
|
+
}
|
|
2457
|
+
catch (error) {
|
|
2458
|
+
throw new Error('getPublicKey', { cause: error });
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// exports
|
|
2464
|
+
export default TeraBoxApp;
|
|
2465
|
+
export { TeraBoxApp };
|