3xui-api-client 2.0.0 → 2.1.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/index.js CHANGED
@@ -1,655 +1,715 @@
1
- const axios = require('axios');
2
- const CredentialGenerator = require('./src/generators/CredentialGenerator');
3
- const { SessionManager, createSessionManager } = require('./src/session/SessionManager');
4
- const {
5
- InputValidator,
6
- SecurityMonitor,
7
- SecureHeaders,
8
- CredentialSecurity,
9
- ErrorSecurity
10
- } = require('./src/security/SecurityEnhancer');
11
-
12
- /**
13
- * 3X-UI API Client Library
14
- *
15
- * A Node.js client for managing 3x-ui panel APIs with automatic session management.
16
- * Now includes credential generation and advanced session management for web applications.
17
- *
18
- * @class ThreeXUI
19
- * @version 2.0.0
20
- * @author Helitha Guruge
21
- */
22
- class ThreeXUI {
23
- /**
24
- * Creates a new ThreeXUI client instance
25
- *
26
- * @param {string} baseURL - The base URL of your 3x-ui server (e.g., 'https://your-server.com')
27
- * @param {string} username - Admin username for authentication
28
- * @param {string} password - Admin password for authentication
29
- * @param {Object} options - Configuration options
30
- * @param {Object} options.sessionManager - Session management configuration
31
- * @param {boolean} options.autoGenerateCredentials - Enable automatic credential generation
32
- * @param {number} options.timeout - Request timeout in milliseconds (default: 30000)
33
- * @throws {Error} If baseURL, username, or password is missing
34
- */
35
- constructor(baseURL, username, password, options = {}) {
36
- if (!baseURL) {
37
- throw new Error('baseURL is required');
38
- }
39
- if (!username) {
40
- throw new Error('username is required');
41
- }
42
- if (!password) {
43
- throw new Error('password is required');
44
- }
45
-
46
- // Apply security validations
47
- this.baseURL = InputValidator.validateURL(baseURL);
48
- this.username = InputValidator.validateUsername(username);
49
- this.password = InputValidator.validatePassword(password);
50
- this.cookie = null;
51
- this.options = options;
52
- this.loginMutex = false; // Add mutex to prevent concurrent logins
53
- this.loginRetryCount = 0; // Add retry counter
54
- this.maxLoginRetries = 3; // Maximum login attempts
55
-
56
- // Initialize security monitoring
57
- this.securityMonitor = new SecurityMonitor({
58
- maxRequestsPerMinute: options.maxRequestsPerMinute || 60,
59
- maxLoginAttemptsPerHour: options.maxLoginAttemptsPerHour || 10
60
- });
61
-
62
- // Security configuration
63
- this.isDevelopment = options.isDevelopment || process.env.NODE_ENV === 'development';
64
-
65
- // Initialize session manager if provided
66
- if (options.sessionManager) {
67
- this.sessionManager = options.sessionManager instanceof SessionManager
68
- ? options.sessionManager
69
- : createSessionManager(options.sessionManager);
70
- } else {
71
- // Default to memory-based session management
72
- this.sessionManager = createSessionManager();
73
- }
74
-
75
- // Create axios instance with security best practices
76
- this.api = axios.create({
77
- baseURL: this.baseURL,
78
- timeout: options.timeout || 30000, // 30 second timeout
79
- maxRedirects: 5,
80
- validateStatus: (status) => status >= 200 && status < 300,
81
- headers: SecureHeaders.getSecureHeaders({
82
- userAgent: options.userAgent || '3xui-api-client/2.0.0 (Security-Enhanced)',
83
- enableCSP: options.enableCSP || false
84
- })
85
- });
86
-
87
- // Add request interceptor for security headers
88
- this.api.interceptors.request.use((config) => {
89
- // Add security headers
90
- config.headers['X-Requested-With'] = 'XMLHttpRequest';
91
- return config;
92
- });
93
-
94
- // Add response interceptor for error handling
95
- this.api.interceptors.response.use(
96
- (response) => response,
97
- (error) => {
98
- if (error.code === 'ECONNABORTED') {
99
- throw new Error('Request timeout - server took too long to respond');
100
- }
101
- if (error.code === 'ENOTFOUND') {
102
- throw new Error(`Cannot connect to server: ${this.baseURL}`);
103
- }
104
- throw error;
105
- }
106
- );
107
- }
108
-
109
- /**
110
- * Login with session management support
111
- * @param {boolean} forceRefresh - Force a new login even if session exists
112
- * @returns {Object} Login response
113
- */
114
- async login(forceRefresh = false) {
115
- // Check rate limiting first
116
- const identifier = CredentialSecurity.hashForLogging(this.username);
117
- if (!this.securityMonitor.checkRateLimit(identifier, 'login')) {
118
- const error = new Error('Rate limit exceeded for login attempts');
119
- ErrorSecurity.logError(error, { username: this.username, baseURL: this.baseURL });
120
- throw error;
121
- }
122
-
123
- // Increment retry count
124
- this.loginRetryCount++;
125
- // Check for existing valid session first
126
- if (!forceRefresh && this.sessionManager) {
127
- const existingSession = await this.sessionManager.getSession(this.baseURL, this.username);
128
- if (existingSession && existingSession.cookie) {
129
- this.cookie = existingSession.cookie;
130
- this.api.defaults.headers.Cookie = this.cookie;
131
- // Reset retry count on successful session restore
132
- this.loginRetryCount = 0;
133
- return {
134
- success: true,
135
- fromCache: true,
136
- data: { msg: 'Session restored from cache' }
137
- };
138
- }
139
- }
140
-
141
- try {
142
- const params = new URLSearchParams();
143
- params.append('username', this.username);
144
- params.append('password', this.password);
145
-
146
- const response = await this.api.post('/login', params, {
147
- headers: {
148
- 'Content-Type': 'application/x-www-form-urlencoded'
149
- }
150
- });
151
-
152
- if (response.data.success) {
153
- const cookies = response.headers['set-cookie'];
154
- if (cookies && cookies.length > 0) {
155
- this.cookie = cookies[0].split(';')[0];
156
- this.api.defaults.headers.Cookie = this.cookie;
157
-
158
- // Store session if session manager is available
159
- if (this.sessionManager) {
160
- try {
161
- await this.sessionManager.storeSession(this.baseURL, this.username, {
162
- cookie: this.cookie,
163
- loginTime: new Date().toISOString()
164
- });
165
- } catch (sessionError) {
166
- console.warn('Failed to store session, continuing without cache:', sessionError.message);
167
- }
168
- }
169
- // Reset retry count on successful login
170
- this.loginRetryCount = 0;
171
- } else {
172
- throw new Error('Login failed: No session cookie received.');
173
- }
174
- return {
175
- success: true,
176
- fromCache: false,
177
- headers: response.headers,
178
- data: response.data
179
- };
180
- } else {
181
- throw new Error(`Login failed: ${response.data.msg}`);
182
- }
183
- } catch (error) {
184
- // Log the error securely
185
- ErrorSecurity.logError(error, {
186
- username: this.username,
187
- baseURL: this.baseURL,
188
- action: 'login'
189
- });
190
-
191
- // Log suspicious activity for multiple failed logins
192
- if (this.loginRetryCount >= this.maxLoginRetries) {
193
- this.securityMonitor.logSuspiciousActivity('multiple_failed_logins', {
194
- username: CredentialSecurity.hashForLogging(this.username),
195
- attempts: this.loginRetryCount
196
- });
197
- }
198
-
199
- // Return sanitized error
200
- const sanitizedError = ErrorSecurity.sanitizeError(error, this.isDevelopment);
201
- throw sanitizedError;
202
- }
203
- }
204
-
205
- /**
206
- * Logout and clear session
207
- */
208
- async logout() {
209
- this.cookie = null;
210
- delete this.api.defaults.headers.Cookie;
211
-
212
- if (this.sessionManager) {
213
- await this.sessionManager.deleteSession(this.baseURL, this.username);
214
- }
215
- }
216
-
217
- async _request(method, path, data = {}) {
218
- // Check session validity first with mutex protection
219
- if (!this.loginMutex && this.sessionManager && !await this.sessionManager.hasValidSession(this.baseURL, this.username)) {
220
- await this._ensureAuthenticated();
221
- } else if (!this.loginMutex && !this.cookie) {
222
- await this._ensureAuthenticated();
223
- }
224
-
225
- try {
226
- const response = await this.api.request({
227
- method,
228
- url: path,
229
- data,
230
- ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
231
- });
232
- // Reset retry counter on successful request
233
- this.loginRetryCount = 0;
234
- return response.data;
235
- } catch (error) {
236
- if (error.response && error.response.status === 401) {
237
- // Cookie might have expired, try to login again with retry limit
238
- if (this.loginRetryCount < this.maxLoginRetries) {
239
- await this._ensureAuthenticated(true); // Force refresh
240
- const response = await this.api.request({
241
- method,
242
- url: path,
243
- data,
244
- ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
245
- });
246
- return response.data;
247
- } else {
248
- throw new Error('Maximum login retry attempts exceeded. Check your credentials.');
249
- }
250
- }
251
- throw error;
252
- }
253
- }
254
-
255
- /**
256
- * Ensure authentication with mutex protection
257
- * @param {boolean} forceRefresh - Force a new login
258
- */
259
- async _ensureAuthenticated(forceRefresh = false) {
260
- // Prevent concurrent login attempts
261
- if (this.loginMutex) {
262
- // Wait for ongoing login to complete
263
- while (this.loginMutex) {
264
- await new Promise(resolve => setTimeout(resolve, 100));
265
- }
266
- return;
267
- }
268
-
269
- this.loginMutex = true;
270
- try {
271
- await this.login(forceRefresh);
272
- } finally {
273
- this.loginMutex = false;
274
- }
275
- }
276
-
277
- // ===========================================
278
- // CREDENTIAL GENERATION METHODS
279
- // ===========================================
280
-
281
- /**
282
- * Generate credentials for a specific protocol
283
- * @param {string} protocol - Protocol name (vless, vmess, trojan, etc.)
284
- * @param {Object} options - Generation options
285
- * @returns {Object} Generated credentials
286
- */
287
- generateCredentials(protocol, options = {}) {
288
- return CredentialGenerator.generateForProtocol(protocol, options);
289
- }
290
-
291
- /**
292
- * Generate UUID for VLESS/VMess protocols
293
- * @param {boolean} secure - Use cryptographically secure generation
294
- * @returns {string} UUID
295
- */
296
- generateUUID(secure = true) {
297
- return secure ? CredentialGenerator.generateSecureUUID() : CredentialGenerator.generateUUID();
298
- }
299
-
300
- /**
301
- * Generate password for Trojan/Shadowsocks protocols
302
- * @param {number} length - Password length
303
- * @param {Object} options - Password options
304
- * @returns {string} Generated password
305
- */
306
- generatePassword(length = 16, options = {}) {
307
- return CredentialGenerator.generatePassword(length, options);
308
- }
309
-
310
- /**
311
- * Generate bulk credentials for multiple clients
312
- * @param {string} protocol - Protocol name
313
- * @param {number} count - Number of credentials to generate
314
- * @param {Object} options - Generation options
315
- * @returns {Array} Array of credentials
316
- */
317
- generateBulkCredentials(protocol, count, options = {}) {
318
- return CredentialGenerator.generateBulk(protocol, count, options);
319
- }
320
-
321
- /**
322
- * Get available cipher methods for Shadowsocks
323
- * @returns {Array} Array of cipher methods
324
- */
325
- getShadowsocksCiphers() {
326
- return CredentialGenerator.getShadowsocksCipherMethods();
327
- }
328
-
329
- /**
330
- * Get recommended cipher for Shadowsocks
331
- * @returns {string} Recommended cipher
332
- */
333
- getRecommendedShadowsocksCipher() {
334
- return CredentialGenerator.getRecommendedShadowsocksCipher();
335
- }
336
-
337
- /**
338
- * Generate WireGuard key pair
339
- * @returns {Object} Key pair with helper methods
340
- */
341
- generateWireGuardKeys() {
342
- return CredentialGenerator.generateWireGuardKeys();
343
- }
344
-
345
- /**
346
- * Generate Reality key pair for anti-censorship
347
- * @returns {Object} Reality key pair with helper methods
348
- */
349
- generateRealityKeys() {
350
- return CredentialGenerator.generateRealityKeys();
351
- }
352
-
353
- /**
354
- * Generate random port number
355
- * @param {number} min - Minimum port
356
- * @param {number} max - Maximum port
357
- * @returns {number} Random port
358
- */
359
- generatePort(min = 10000, max = 65535) {
360
- return CredentialGenerator.generatePort(min, max);
361
- }
362
-
363
- /**
364
- * Validate generated credentials
365
- * @param {Object} credentials - Credentials to validate
366
- * @param {string} protocol - Protocol name
367
- * @returns {Object} Validation result
368
- */
369
- validateCredentials(credentials, protocol) {
370
- return CredentialGenerator.validateCredentials(credentials, protocol);
371
- }
372
-
373
- // ===========================================
374
- // ENHANCED CLIENT MANAGEMENT WITH AUTO-GENERATION
375
- // ===========================================
376
-
377
- /**
378
- * Add client with automatic credential generation
379
- * @param {number} inboundId - Inbound ID
380
- * @param {string} protocol - Protocol type
381
- * @param {Object} options - Client options
382
- * @returns {Object} Created client with credentials
383
- */
384
- async addClientWithCredentials(inboundId, protocol, options = {}) {
385
- const credentials = this.generateCredentials(protocol, options);
386
-
387
- const clientConfig = {
388
- id: inboundId,
389
- settings: JSON.stringify({
390
- clients: [{
391
- ...credentials,
392
- enable: true,
393
- expiryTime: options.expiryTime || 0,
394
- limitIp: options.limitIp || 0,
395
- totalGB: options.totalGB || 0,
396
- subId: options.subId || this.generateUUID()
397
- }]
398
- })
399
- };
400
-
401
- const result = await this.addClient(clientConfig);
402
- return {
403
- ...result,
404
- credentials,
405
- protocol
406
- };
407
- }
408
-
409
- /**
410
- * Update client with automatic credential management
411
- * @param {string} clientId - Client UUID/ID
412
- * @param {number} inboundId - Inbound ID
413
- * @param {Object} options - Update options
414
- * @returns {Object} Update result
415
- */
416
- async updateClientWithCredentials(clientId, inboundId, options = {}) {
417
- try {
418
- // First, get the current inbound to obtain all existing clients
419
- const inboundData = await this.getInbound(inboundId);
420
-
421
- if (!inboundData.success || !inboundData.obj) {
422
- throw new Error(`Failed to get inbound ${inboundId} for client update`);
423
- }
424
-
425
- // Parse existing settings to get all clients
426
- const currentSettings = JSON.parse(inboundData.obj.settings);
427
- const existingClients = currentSettings.clients || [];
428
-
429
- // Find the client to update
430
- const clientIndex = existingClients.findIndex(client => client.id === clientId);
431
- if (clientIndex === -1) {
432
- throw new Error(`Client with ID ${clientId} not found in inbound ${inboundId}`);
433
- }
434
-
435
- // Convert user-friendly options to API format
436
- const processedOptions = {
437
- email: options.email || existingClients[clientIndex].email,
438
- limitIp: options.limitIp !== undefined ? options.limitIp : existingClients[clientIndex].limitIp,
439
- totalGB: options.totalGB ? options.totalGB * 1024 * 1024 * 1024 : existingClients[clientIndex].totalGB,
440
- expiryTime: options.expiryDays ? Date.now() + (options.expiryDays * 24 * 60 * 60 * 1000) : existingClients[clientIndex].expiryTime,
441
- enable: options.enable !== undefined ? options.enable : existingClients[clientIndex].enable,
442
- flow: options.flow || existingClients[clientIndex].flow,
443
- encryption: options.encryption || existingClients[clientIndex].encryption || 'none',
444
- subId: options.subId || existingClients[clientIndex].subId
445
- };
446
-
447
- // Update the specific client while preserving others
448
- existingClients[clientIndex] = {
449
- ...existingClients[clientIndex],
450
- ...processedOptions
451
- };
452
-
453
- // Prepare the complete settings with all clients
454
- const updatedSettings = {
455
- ...currentSettings,
456
- clients: existingClients
457
- };
458
-
459
- const clientConfig = {
460
- id: inboundId,
461
- settings: JSON.stringify(updatedSettings)
462
- };
463
-
464
- const result = await this.updateClient(clientId, clientConfig);
465
- return {
466
- ...result,
467
- updatedOptions: processedOptions,
468
- conversions: {
469
- totalGB: options.totalGB ? `${options.totalGB}GB → ${processedOptions.totalGB} bytes` : 'unchanged',
470
- expiryDays: options.expiryDays ? `${options.expiryDays} days → ${new Date(processedOptions.expiryTime).toISOString()}` : 'unchanged'
471
- }
472
- };
473
- } catch (error) {
474
- return {
475
- success: false,
476
- message: error.message,
477
- error: error.message,
478
- details: 'updateClientWithCredentials failed - check client ID and inbound ID'
479
- };
480
- }
481
- }
482
-
483
- // ===========================================
484
- // SESSION MANAGEMENT METHODS
485
- // ===========================================
486
-
487
- /**
488
- * Get session statistics
489
- * @returns {Object} Session statistics
490
- */
491
- async getSessionStats() {
492
- if (this.sessionManager) {
493
- return await this.sessionManager.getStats();
494
- }
495
- return { message: 'Session manager not initialized' };
496
- }
497
-
498
- /**
499
- * Clear all cached sessions
500
- */
501
- async clearAllSessions() {
502
- if (this.sessionManager) {
503
- await this.sessionManager.clearAllSessions();
504
- }
505
- }
506
-
507
- /**
508
- * Check if current session is valid
509
- * @returns {boolean} Session validity
510
- */
511
- async isSessionValid() {
512
- if (this.sessionManager) {
513
- return await this.sessionManager.hasValidSession(this.baseURL, this.username);
514
- }
515
- return !!this.cookie;
516
- }
517
-
518
- // ===========================================
519
- // ORIGINAL API METHODS (UNCHANGED)
520
- // ===========================================
521
-
522
- // Inbounds
523
- getInbounds() {
524
- return this._request('get', '/panel/api/inbounds/list');
525
- }
526
-
527
- getInbound(id) {
528
- return this._request('get', `/panel/api/inbounds/get/${id}`);
529
- }
530
-
531
- addInbound(inboundConfig) {
532
- // Validate inbound configuration for security
533
- const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
534
- return this._request('post', '/panel/api/inbounds/add', validatedConfig);
535
- }
536
-
537
- deleteInbound(id) {
538
- return this._request('post', `/panel/api/inbounds/del/${id}`);
539
- }
540
-
541
- updateInbound(id, inboundConfig) {
542
- // Validate inbound configuration for security
543
- const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
544
- return this._request('post', `/panel/api/inbounds/update/${id}`, validatedConfig);
545
- }
546
-
547
- // Clients
548
- addClient(clientConfig) {
549
- // Validate client configuration for security
550
- const validatedConfig = InputValidator.validateClientConfig(clientConfig);
551
- return this._request('post', '/panel/api/inbounds/addClient', validatedConfig);
552
- }
553
-
554
- deleteClient(inboundId, clientId) {
555
- return this._request('post', `/panel/api/inbounds/${inboundId}/delClient/${clientId}`);
556
- }
557
-
558
- updateClient(clientId, clientConfig) {
559
- // Validate client configuration for security
560
- const validatedConfig = InputValidator.validateClientConfig(clientConfig);
561
- return this._request('post', `/panel/api/inbounds/updateClient/${clientId}`, validatedConfig);
562
- }
563
-
564
- getClientTrafficsByEmail(email) {
565
- return this._request('get', `/panel/api/inbounds/getClientTraffics/${email}`);
566
- }
567
-
568
- getClientTrafficsById(id) {
569
- return this._request('get', `/panel/api/inbounds/getClientTrafficsById/${id}`);
570
- }
571
-
572
- getClientIps(email) {
573
- return this._request('post', `/panel/api/inbounds/clientIps/${email}`);
574
- }
575
-
576
- clearClientIps(email) {
577
- return this._request('post', `/panel/api/inbounds/clearClientIps/${email}`);
578
- }
579
-
580
- // Traffic
581
- resetClientTraffic(inboundId, email) {
582
- return this._request('post', `/panel/api/inbounds/${inboundId}/resetClientTraffic/${email}`);
583
- }
584
-
585
- resetAllTraffics() {
586
- return this._request('post', '/panel/api/inbounds/resetAllTraffics');
587
- }
588
-
589
- resetAllClientTraffics(inboundId) {
590
- return this._request('post', `/panel/api/inbounds/resetAllClientTraffics/${inboundId}`);
591
- }
592
-
593
- deleteDepletedClients(inboundId) {
594
- return this._request('post', `/panel/api/inbounds/delDepletedClients/${inboundId}`);
595
- }
596
-
597
- // System
598
- getOnlineClients() {
599
- return this._request('post', '/panel/api/inbounds/onlines');
600
- }
601
-
602
- createBackup() {
603
- return this._request('get', '/panel/api/inbounds/createbackup');
604
- }
605
-
606
- // Security Methods
607
-
608
- /**
609
- * Get security statistics and monitoring data
610
- * @returns {Object} Security statistics
611
- */
612
- getSecurityStats() {
613
- return this.securityMonitor.getStats();
614
- }
615
-
616
- /**
617
- * Clear blocked IPs (admin function)
618
- */
619
- clearBlockedIPs() {
620
- this.securityMonitor.clearBlockedIPs();
621
- }
622
-
623
- /**
624
- * Validate credential strength
625
- * @param {string} credential - Credential to validate
626
- * @param {string} type - Type of credential
627
- * @returns {Object} Validation result
628
- */
629
- validateCredentialStrength(credential, type) {
630
- return CredentialSecurity.validateCredentialStrength(credential, type);
631
- }
632
-
633
- /**
634
- * Generate secure session token
635
- * @returns {string} Secure session token
636
- */
637
- generateSecureToken() {
638
- return CredentialSecurity.generateSessionToken();
639
- }
640
-
641
- /**
642
- * Enable development mode for detailed error messages
643
- * @param {boolean} enabled - Whether to enable development mode
644
- */
645
- setDevelopmentMode(enabled) {
646
- this.isDevelopment = enabled;
647
- }
648
- }
649
-
650
- // Export static methods for standalone use
651
- ThreeXUI.CredentialGenerator = CredentialGenerator;
652
- ThreeXUI.SessionManager = SessionManager;
653
- ThreeXUI.createSessionManager = createSessionManager;
654
-
655
- module.exports = ThreeXUI;
1
+ const axios = require('axios');
2
+ const CredentialGenerator = require('./src/generators/CredentialGenerator');
3
+ const { SessionManager, createSessionManager } = require('./src/session/SessionManager');
4
+ const {
5
+ InputValidator,
6
+ SecurityMonitor,
7
+ SecureHeaders,
8
+ CredentialSecurity,
9
+ ErrorSecurity
10
+ } = require('./src/security/SecurityEnhancer');
11
+
12
+ /**
13
+ * 3X-UI API Client Library
14
+ *
15
+ * A Node.js client for managing 3x-ui panel APIs with automatic session management.
16
+ * Now includes credential generation and advanced session management for web applications.
17
+ *
18
+ * @class ThreeXUI
19
+ * @version 2.0.0
20
+ * @author Helitha Guruge
21
+ */
22
+ class ThreeXUI {
23
+ /**
24
+ * Creates a new ThreeXUI client instance
25
+ *
26
+ * @param {string} baseURL - The base URL of your 3x-ui server (e.g., 'https://your-server.com')
27
+ * @param {string} username - Admin username for authentication
28
+ * @param {string} password - Admin password for authentication
29
+ * @param {Object} options - Configuration options
30
+ * @param {Object} options.sessionManager - Session management configuration
31
+ * @param {boolean} options.autoGenerateCredentials - Enable automatic credential generation
32
+ * @param {number} options.timeout - Request timeout in milliseconds (default: 30000)
33
+ * @throws {Error} If baseURL, username, or password is missing
34
+ */
35
+ constructor(baseURL, username, password, options = {}) {
36
+ if (!baseURL) {
37
+ throw new Error('baseURL is required');
38
+ }
39
+ if (!username) {
40
+ throw new Error('username is required');
41
+ }
42
+ if (!password) {
43
+ throw new Error('password is required');
44
+ }
45
+
46
+ // Apply security validations
47
+ this.baseURL = InputValidator.validateURL(baseURL);
48
+ this.username = InputValidator.validateUsername(username);
49
+ this.password = InputValidator.validatePassword(password);
50
+ this.cookie = null;
51
+ this.options = options;
52
+ this.loginMutex = false; // Add mutex to prevent concurrent logins
53
+ this.loginRetryCount = 0; // Add retry counter
54
+ this.maxLoginRetries = 3; // Maximum login attempts
55
+
56
+ // Initialize security monitoring
57
+ this.securityMonitor = new SecurityMonitor({
58
+ maxRequestsPerMinute: options.maxRequestsPerMinute || 60,
59
+ maxLoginAttemptsPerHour: options.maxLoginAttemptsPerHour || 10
60
+ });
61
+
62
+ // Security configuration
63
+ this.isDevelopment = options.isDevelopment || process.env.NODE_ENV === 'development';
64
+
65
+ // Initialize session manager if provided
66
+ if (options.sessionManager) {
67
+ this.sessionManager = options.sessionManager instanceof SessionManager
68
+ ? options.sessionManager
69
+ : createSessionManager(options.sessionManager);
70
+ } else {
71
+ // Default to memory-based session management
72
+ this.sessionManager = createSessionManager();
73
+ }
74
+
75
+ // Create axios instance with security best practices
76
+ this.api = axios.create({
77
+ baseURL: this.baseURL,
78
+ timeout: options.timeout || 30000, // 30 second timeout
79
+ maxRedirects: 5,
80
+ validateStatus: (status) => status >= 200 && status < 300,
81
+ headers: SecureHeaders.getSecureHeaders({
82
+ userAgent: options.userAgent || '3xui-api-client/2.0.0 (Security-Enhanced)',
83
+ enableCSP: options.enableCSP || false
84
+ })
85
+ });
86
+
87
+ // Add request interceptor for security headers
88
+ this.api.interceptors.request.use((config) => {
89
+ // Add security headers
90
+ config.headers['X-Requested-With'] = 'XMLHttpRequest';
91
+ return config;
92
+ });
93
+
94
+ // Add response interceptor for error handling
95
+ this.api.interceptors.response.use(
96
+ (response) => response,
97
+ (error) => {
98
+ if (error.code === 'ECONNABORTED') {
99
+ throw new Error('Request timeout - server took too long to respond');
100
+ }
101
+ if (error.code === 'ENOTFOUND') {
102
+ throw new Error(`Cannot connect to server: ${this.baseURL}`);
103
+ }
104
+ throw error;
105
+ }
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Login with session management support
111
+ * @param {boolean} forceRefresh - Force a new login even if session exists
112
+ * @returns {Object} Login response
113
+ */
114
+ async login(forceRefresh = false) {
115
+ // Check rate limiting first
116
+ const identifier = CredentialSecurity.hashForLogging(this.username);
117
+ if (!this.securityMonitor.checkRateLimit(identifier, 'login')) {
118
+ const error = new Error('Rate limit exceeded for login attempts');
119
+ ErrorSecurity.logError(error, { username: this.username, baseURL: this.baseURL });
120
+ throw error;
121
+ }
122
+
123
+ // Increment retry count
124
+ this.loginRetryCount++;
125
+ // Check for existing valid session first
126
+ if (!forceRefresh && this.sessionManager) {
127
+ const existingSession = await this.sessionManager.getSession(this.baseURL, this.username);
128
+ if (existingSession && existingSession.cookie) {
129
+ this.cookie = existingSession.cookie;
130
+ this.api.defaults.headers.Cookie = this.cookie;
131
+ // Reset retry count on successful session restore
132
+ this.loginRetryCount = 0;
133
+ return {
134
+ success: true,
135
+ fromCache: true,
136
+ data: { msg: 'Session restored from cache' }
137
+ };
138
+ }
139
+ }
140
+
141
+ try {
142
+ const params = new URLSearchParams();
143
+ params.append('username', this.username);
144
+ params.append('password', this.password);
145
+
146
+ const response = await this.api.post('/login', params, {
147
+ headers: {
148
+ 'Content-Type': 'application/x-www-form-urlencoded'
149
+ }
150
+ });
151
+
152
+ if (response.data.success) {
153
+ const cookies = response.headers['set-cookie'];
154
+ if (cookies && cookies.length > 0) {
155
+ this.cookie = cookies[0].split(';')[0];
156
+ this.api.defaults.headers.Cookie = this.cookie;
157
+
158
+ // Store session if session manager is available
159
+ if (this.sessionManager) {
160
+ try {
161
+ await this.sessionManager.storeSession(this.baseURL, this.username, {
162
+ cookie: this.cookie,
163
+ loginTime: new Date().toISOString()
164
+ });
165
+ } catch (sessionError) {
166
+ console.warn('Failed to store session, continuing without cache:', sessionError.message);
167
+ }
168
+ }
169
+ // Reset retry count on successful login
170
+ this.loginRetryCount = 0;
171
+ } else {
172
+ throw new Error('Login failed: No session cookie received.');
173
+ }
174
+ return {
175
+ success: true,
176
+ fromCache: false,
177
+ headers: response.headers,
178
+ data: response.data
179
+ };
180
+ } else {
181
+ throw new Error(`Login failed: ${response.data.msg}`);
182
+ }
183
+ } catch (error) {
184
+ // Log the error securely
185
+ ErrorSecurity.logError(error, {
186
+ username: this.username,
187
+ baseURL: this.baseURL,
188
+ action: 'login'
189
+ });
190
+
191
+ // Log suspicious activity for multiple failed logins
192
+ if (this.loginRetryCount >= this.maxLoginRetries) {
193
+ this.securityMonitor.logSuspiciousActivity('multiple_failed_logins', {
194
+ username: CredentialSecurity.hashForLogging(this.username),
195
+ attempts: this.loginRetryCount
196
+ });
197
+ }
198
+
199
+ // Return sanitized error
200
+ const sanitizedError = ErrorSecurity.sanitizeError(error, this.isDevelopment);
201
+ throw sanitizedError;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Logout and clear session
207
+ */
208
+ async logout() {
209
+ this.cookie = null;
210
+ delete this.api.defaults.headers.Cookie;
211
+
212
+ if (this.sessionManager) {
213
+ await this.sessionManager.deleteSession(this.baseURL, this.username);
214
+ }
215
+ }
216
+
217
+ async _request(method, path, data = {}) {
218
+ // Check session validity first with mutex protection
219
+ if (!this.loginMutex && this.sessionManager && !await this.sessionManager.hasValidSession(this.baseURL, this.username)) {
220
+ await this._ensureAuthenticated();
221
+ } else if (!this.loginMutex && !this.cookie) {
222
+ await this._ensureAuthenticated();
223
+ }
224
+
225
+ try {
226
+ const response = await this.api.request({
227
+ method,
228
+ url: path,
229
+ data,
230
+ ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
231
+ });
232
+ // Reset retry counter on successful request
233
+ this.loginRetryCount = 0;
234
+ return response.data;
235
+ } catch (error) {
236
+ if (error.response && error.response.status === 401) {
237
+ // Cookie might have expired, try to login again with retry limit
238
+ if (this.loginRetryCount < this.maxLoginRetries) {
239
+ await this._ensureAuthenticated(true); // Force refresh
240
+ const response = await this.api.request({
241
+ method,
242
+ url: path,
243
+ data,
244
+ ...(method.toLowerCase() === 'post' ? { headers: { 'Content-Type': 'application/json' } } : {})
245
+ });
246
+ return response.data;
247
+ } else {
248
+ throw new Error('Maximum login retry attempts exceeded. Check your credentials.');
249
+ }
250
+ }
251
+ throw error;
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Ensure authentication with mutex protection
257
+ * @param {boolean} forceRefresh - Force a new login
258
+ */
259
+ async _ensureAuthenticated(forceRefresh = false) {
260
+ // Prevent concurrent login attempts
261
+ if (this.loginMutex) {
262
+ // Wait for ongoing login to complete
263
+ while (this.loginMutex) {
264
+ await new Promise(resolve => setTimeout(resolve, 100));
265
+ }
266
+ return;
267
+ }
268
+
269
+ this.loginMutex = true;
270
+ try {
271
+ await this.login(forceRefresh);
272
+ } finally {
273
+ this.loginMutex = false;
274
+ }
275
+ }
276
+
277
+ // ===========================================
278
+ // CREDENTIAL GENERATION METHODS
279
+ // ===========================================
280
+
281
+ /**
282
+ * Generate credentials for a specific protocol
283
+ * @param {string} protocol - Protocol name (vless, vmess, trojan, etc.)
284
+ * @param {Object} options - Generation options
285
+ * @returns {Object} Generated credentials
286
+ */
287
+ generateCredentials(protocol, options = {}) {
288
+ return CredentialGenerator.generateForProtocol(protocol, options);
289
+ }
290
+
291
+ /**
292
+ * Generate UUID for VLESS/VMess protocols
293
+ * @param {boolean} secure - Use cryptographically secure generation
294
+ * @returns {string} UUID
295
+ */
296
+ generateUUID(secure = true) {
297
+ return secure ? CredentialGenerator.generateSecureUUID() : CredentialGenerator.generateUUID();
298
+ }
299
+
300
+ /**
301
+ * Generate password for Trojan/Shadowsocks protocols
302
+ * @param {number} length - Password length
303
+ * @param {Object} options - Password options
304
+ * @returns {string} Generated password
305
+ */
306
+ generatePassword(length = 16, options = {}) {
307
+ return CredentialGenerator.generatePassword(length, options);
308
+ }
309
+
310
+ /**
311
+ * Generate bulk credentials for multiple clients
312
+ * @param {string} protocol - Protocol name
313
+ * @param {number} count - Number of credentials to generate
314
+ * @param {Object} options - Generation options
315
+ * @returns {Array} Array of credentials
316
+ */
317
+ generateBulkCredentials(protocol, count, options = {}) {
318
+ return CredentialGenerator.generateBulk(protocol, count, options);
319
+ }
320
+
321
+ /**
322
+ * Get available cipher methods for Shadowsocks
323
+ * @returns {Array} Array of cipher methods
324
+ */
325
+ getShadowsocksCiphers() {
326
+ return CredentialGenerator.getShadowsocksCipherMethods();
327
+ }
328
+
329
+ /**
330
+ * Get recommended cipher for Shadowsocks
331
+ * @returns {string} Recommended cipher
332
+ */
333
+ getRecommendedShadowsocksCipher() {
334
+ return CredentialGenerator.getRecommendedShadowsocksCipher();
335
+ }
336
+
337
+ /**
338
+ * Generate WireGuard key pair
339
+ * @returns {Object} Key pair with helper methods
340
+ */
341
+ generateWireGuardKeys() {
342
+ return CredentialGenerator.generateWireGuardKeys();
343
+ }
344
+
345
+ /**
346
+ * Generate Reality key pair for anti-censorship
347
+ * @returns {Object} Reality key pair with helper methods
348
+ */
349
+ generateRealityKeys() {
350
+ return CredentialGenerator.generateRealityKeys();
351
+ }
352
+
353
+ /**
354
+ * Generate random port number
355
+ * @param {number} min - Minimum port
356
+ * @param {number} max - Maximum port
357
+ * @returns {number} Random port
358
+ */
359
+ generatePort(min = 10000, max = 65535) {
360
+ return CredentialGenerator.generatePort(min, max);
361
+ }
362
+
363
+ /**
364
+ * Validate generated credentials
365
+ * @param {Object} credentials - Credentials to validate
366
+ * @param {string} protocol - Protocol name
367
+ * @returns {Object} Validation result
368
+ */
369
+ validateCredentials(credentials, protocol) {
370
+ return CredentialGenerator.validateCredentials(credentials, protocol);
371
+ }
372
+
373
+ // ===========================================
374
+ // ENHANCED CLIENT MANAGEMENT WITH AUTO-GENERATION
375
+ // ===========================================
376
+
377
+ /**
378
+ * Add client with automatic credential generation
379
+ * @param {number} inboundId - Inbound ID
380
+ * @param {string} protocol - Protocol type
381
+ * @param {Object} options - Client options
382
+ * @returns {Object} Created client with credentials
383
+ */
384
+ async addClientWithCredentials(inboundId, protocol, options = {}) {
385
+ const credentials = this.generateCredentials(protocol, options);
386
+
387
+ const clientConfig = {
388
+ id: inboundId,
389
+ settings: JSON.stringify({
390
+ clients: [{
391
+ ...credentials,
392
+ enable: true,
393
+ expiryTime: options.expiryTime || 0,
394
+ limitIp: options.limitIp || 0,
395
+ totalGB: options.totalGB || 0,
396
+ subId: options.subId || this.generateUUID()
397
+ }]
398
+ })
399
+ };
400
+
401
+ const result = await this.addClient(clientConfig);
402
+ return {
403
+ ...result,
404
+ credentials,
405
+ protocol
406
+ };
407
+ }
408
+
409
+ /**
410
+ * Update client with automatic credential management
411
+ * @param {string} clientId - Client UUID/ID
412
+ * @param {number} inboundId - Inbound ID
413
+ * @param {Object} options - Update options
414
+ * @returns {Object} Update result
415
+ */
416
+ async updateClientWithCredentials(clientId, inboundId, options = {}) {
417
+ try {
418
+ // First, get the current inbound to obtain all existing clients
419
+ const inboundData = await this.getInbound(inboundId);
420
+
421
+ if (!inboundData.success || !inboundData.obj) {
422
+ throw new Error(`Failed to get inbound ${inboundId} for client update`);
423
+ }
424
+
425
+ // Parse existing settings to get all clients
426
+ const currentSettings = JSON.parse(inboundData.obj.settings);
427
+ const existingClients = currentSettings.clients || [];
428
+
429
+ // Find the client to update
430
+ const clientIndex = existingClients.findIndex(client => client.id === clientId);
431
+ if (clientIndex === -1) {
432
+ throw new Error(`Client with ID ${clientId} not found in inbound ${inboundId}`);
433
+ }
434
+
435
+ // Convert user-friendly options to API format
436
+ const processedOptions = {
437
+ email: options.email || existingClients[clientIndex].email,
438
+ limitIp: options.limitIp !== undefined ? options.limitIp : existingClients[clientIndex].limitIp,
439
+ // totalGB is specified in gigabytes in 3x-ui config; do not convert to bytes
440
+ totalGB: options.totalGB !== undefined ? options.totalGB : existingClients[clientIndex].totalGB,
441
+ expiryTime: options.expiryDays ? Date.now() + (options.expiryDays * 24 * 60 * 60 * 1000) : existingClients[clientIndex].expiryTime,
442
+ enable: options.enable !== undefined ? options.enable : existingClients[clientIndex].enable,
443
+ flow: options.flow || existingClients[clientIndex].flow,
444
+ encryption: options.encryption || existingClients[clientIndex].encryption || 'none',
445
+ subId: options.subId || existingClients[clientIndex].subId
446
+ };
447
+
448
+ // Update the specific client while preserving others
449
+ existingClients[clientIndex] = {
450
+ ...existingClients[clientIndex],
451
+ ...processedOptions
452
+ };
453
+
454
+ // Prepare the complete settings with all clients
455
+ const updatedSettings = {
456
+ ...currentSettings,
457
+ clients: existingClients
458
+ };
459
+
460
+ const clientConfig = {
461
+ id: inboundId,
462
+ settings: JSON.stringify(updatedSettings)
463
+ };
464
+
465
+ const result = await this.updateClient(clientId, clientConfig);
466
+ return {
467
+ ...result,
468
+ updatedOptions: processedOptions,
469
+ conversions: {
470
+ totalGB: options.totalGB !== undefined ? `${options.totalGB}GB` : 'unchanged',
471
+ expiryDays: options.expiryDays ? `${options.expiryDays} days → ${new Date(processedOptions.expiryTime).toISOString()}` : 'unchanged'
472
+ }
473
+ };
474
+ } catch (error) {
475
+ return {
476
+ success: false,
477
+ message: error.message,
478
+ error: error.message,
479
+ details: 'updateClientWithCredentials failed - check client ID and inbound ID'
480
+ };
481
+ }
482
+ }
483
+
484
+ // ===========================================
485
+ // SESSION MANAGEMENT METHODS
486
+ // ===========================================
487
+
488
+ /**
489
+ * Get session statistics
490
+ * @returns {Object} Session statistics
491
+ */
492
+ async getSessionStats() {
493
+ if (this.sessionManager) {
494
+ return await this.sessionManager.getStats();
495
+ }
496
+ return { message: 'Session manager not initialized' };
497
+ }
498
+
499
+ /**
500
+ * Clear all cached sessions
501
+ */
502
+ async clearAllSessions() {
503
+ if (this.sessionManager) {
504
+ await this.sessionManager.clearAllSessions();
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Check if current session is valid
510
+ * @returns {boolean} Session validity
511
+ */
512
+ async isSessionValid() {
513
+ if (this.sessionManager) {
514
+ return await this.sessionManager.hasValidSession(this.baseURL, this.username);
515
+ }
516
+ return !!this.cookie;
517
+ }
518
+
519
+ // ===========================================
520
+ // ORIGINAL API METHODS (UNCHANGED)
521
+ // ===========================================
522
+
523
+ // Inbounds
524
+ getInbounds() {
525
+ return this._request('get', '/panel/api/inbounds/list');
526
+ }
527
+
528
+ getInbound(id) {
529
+ return this._request('get', `/panel/api/inbounds/get/${id}`);
530
+ }
531
+
532
+ addInbound(inboundConfig) {
533
+ // Validate inbound configuration for security
534
+ const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
535
+ return this._request('post', '/panel/api/inbounds/add', validatedConfig);
536
+ }
537
+
538
+ deleteInbound(id) {
539
+ return this._request('post', `/panel/api/inbounds/del/${id}`);
540
+ }
541
+
542
+ updateInbound(id, inboundConfig) {
543
+ // Validate inbound configuration for security
544
+ const validatedConfig = InputValidator.validateInboundConfig(inboundConfig);
545
+ return this._request('post', `/panel/api/inbounds/update/${id}`, validatedConfig);
546
+ }
547
+
548
+ // Clients
549
+ addClient(clientConfig) {
550
+ // Validate client configuration for security
551
+ const validatedConfig = InputValidator.validateClientConfig(clientConfig);
552
+ return this._request('post', '/panel/api/inbounds/addClient', validatedConfig);
553
+ }
554
+
555
+ deleteClient(inboundId, clientId) {
556
+ return this._request('post', `/panel/api/inbounds/${inboundId}/delClient/${clientId}`);
557
+ }
558
+
559
+ updateClient(clientId, clientConfig) {
560
+ // Validate client configuration for security
561
+ const validatedConfig = InputValidator.validateClientConfig(clientConfig);
562
+ return this._request('post', `/panel/api/inbounds/updateClient/${clientId}`, validatedConfig);
563
+ }
564
+
565
+ getClientTrafficsByEmail(email) {
566
+ return this._request('get', `/panel/api/inbounds/getClientTraffics/${email}`);
567
+ }
568
+
569
+ getClientTrafficsById(id) {
570
+ return this._request('get', `/panel/api/inbounds/getClientTrafficsById/${id}`);
571
+ }
572
+
573
+ getClientIps(email) {
574
+ return this._request('post', `/panel/api/inbounds/clientIps/${email}`);
575
+ }
576
+
577
+ clearClientIps(email) {
578
+ return this._request('post', `/panel/api/inbounds/clearClientIps/${email}`);
579
+ }
580
+
581
+ // Traffic
582
+ resetClientTraffic(inboundId, email) {
583
+ return this._request('post', `/panel/api/inbounds/${inboundId}/resetClientTraffic/${email}`);
584
+ }
585
+
586
+ resetAllTraffics() {
587
+ return this._request('post', '/panel/api/inbounds/resetAllTraffics');
588
+ }
589
+
590
+ resetAllClientTraffics(inboundId) {
591
+ return this._request('post', `/panel/api/inbounds/resetAllClientTraffics/${inboundId}`);
592
+ }
593
+
594
+ deleteDepletedClients(inboundId) {
595
+ return this._request('post', `/panel/api/inbounds/delDepletedClients/${inboundId}`);
596
+ }
597
+
598
+ // System
599
+ getOnlineClients() {
600
+ return this._request('post', '/panel/api/inbounds/onlines');
601
+ }
602
+
603
+ createBackup() {
604
+ return this._request('get', '/panel/api/inbounds/createbackup');
605
+ }
606
+
607
+ // Security Methods
608
+
609
+ /**
610
+ * Get security statistics and monitoring data
611
+ * @returns {Object} Security statistics
612
+ */
613
+ getSecurityStats() {
614
+ return this.securityMonitor.getStats();
615
+ }
616
+
617
+ /**
618
+ * Clear blocked IPs (admin function)
619
+ */
620
+ clearBlockedIPs() {
621
+ this.securityMonitor.clearBlockedIPs();
622
+ }
623
+
624
+ /**
625
+ * Validate credential strength
626
+ * @param {string} credential - Credential to validate
627
+ * @param {string} type - Type of credential
628
+ * @returns {Object} Validation result
629
+ */
630
+ validateCredentialStrength(credential, type) {
631
+ return CredentialSecurity.validateCredentialStrength(credential, type);
632
+ }
633
+
634
+ /**
635
+ * Generate secure session token
636
+ * @returns {string} Secure session token
637
+ */
638
+ generateSecureToken() {
639
+ return CredentialSecurity.generateSessionToken();
640
+ }
641
+
642
+ /**
643
+ * Enable development mode for detailed error messages
644
+ * @param {boolean} enabled - Whether to enable development mode
645
+ */
646
+ setDevelopmentMode(enabled) {
647
+ this.isDevelopment = enabled;
648
+ }
649
+ }
650
+
651
+ // Export static methods for standalone use
652
+ ThreeXUI.CredentialGenerator = CredentialGenerator;
653
+ ThreeXUI.SessionManager = SessionManager;
654
+ ThreeXUI.createSessionManager = createSessionManager;
655
+
656
+ module.exports = ThreeXUI;
657
+
658
+ // Define lazy getters to avoid circular dependencies
659
+ Object.defineProperties(module.exports, {
660
+ // Web middleware helpers
661
+ createExpressMiddleware: {
662
+ enumerable: true,
663
+ get: () => require('./src/middleware/WebMiddleware').createExpressMiddleware
664
+ },
665
+ withThreeXUI: {
666
+ enumerable: true,
667
+ get: () => require('./src/middleware/WebMiddleware').withThreeXUI
668
+ },
669
+ createReactHook: {
670
+ enumerable: true,
671
+ get: () => require('./src/middleware/WebMiddleware').createReactHook
672
+ },
673
+ createNextjsRoutes: {
674
+ enumerable: true,
675
+ get: () => require('./src/middleware/WebMiddleware').createNextjsRoutes
676
+ },
677
+ SessionConfig: {
678
+ enumerable: true,
679
+ get: () => require('./src/middleware/WebMiddleware').SessionConfig
680
+ },
681
+ // Protocol builders
682
+ ProtocolBuilder: {
683
+ enumerable: true,
684
+ get: () => require('./src/builders/ProtocolBuilders').ProtocolBuilder
685
+ },
686
+ VLESSBuilder: {
687
+ enumerable: true,
688
+ get: () => require('./src/builders/ProtocolBuilders').VLESSBuilder
689
+ },
690
+ VMESSBuilder: {
691
+ enumerable: true,
692
+ get: () => require('./src/builders/ProtocolBuilders').VMESSBuilder
693
+ },
694
+ TrojanBuilder: {
695
+ enumerable: true,
696
+ get: () => require('./src/builders/ProtocolBuilders').TrojanBuilder
697
+ },
698
+ ShadowsocksBuilder: {
699
+ enumerable: true,
700
+ get: () => require('./src/builders/ProtocolBuilders').ShadowsocksBuilder
701
+ },
702
+ WireGuardBuilder: {
703
+ enumerable: true,
704
+ get: () => require('./src/builders/ProtocolBuilders').WireGuardBuilder
705
+ },
706
+ BaseBuilder: {
707
+ enumerable: true,
708
+ get: () => require('./src/builders/ProtocolBuilders').BaseBuilder
709
+ },
710
+ // Security helpers
711
+ SecurityEnhancer: {
712
+ enumerable: true,
713
+ get: () => require('./src/security/SecurityEnhancer')
714
+ }
715
+ });