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/CHANGELOG.md +28 -0
- package/index.d.ts +437 -416
- package/index.js +715 -655
- package/package.json +113 -113
- package/src/builders/ProtocolBuilders.js +5 -1
- package/src/security/SecurityEnhancer.js +325 -1
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
...
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
*
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
*
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
*
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
*
|
|
626
|
-
* @param {string}
|
|
627
|
-
* @
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
*
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
*
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
ThreeXUI.
|
|
653
|
-
ThreeXUI.
|
|
654
|
-
|
|
655
|
-
|
|
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
|
+
});
|