UNPKG

js3xui

Version:

Async Object-oriented JavaScript SDK for the 3x-ui API.

217 lines (196 loc) 8.71 kB
import axios from 'axios'; import { wrapper } from 'axios-cookiejar-support'; import { CookieJar } from 'tough-cookie'; import ApiFields from './ApiFields.js'; /** * Base class for the XUI API. Contains common methods for making requests. */ class BaseApi { /** * Creates an instance of BaseApi. * @param {string} host - The host URL * @param {string} username - The username for authentication * @param {string} password - The password for authentication * @param {boolean} [useTlsVerify=true] - Whether to verify TLS certificates * @param {string|null} [customCertificatePath=null] - Path to custom certificate * @param {*} [logger=null] - Optional logger instance * @param {CookieJar} [sharedCookieJar=null] - Optional shared cookie jar * @param {*} [sharedAxiosInstance=null] - Optional shared axios instance */ constructor( host, username, password, useTlsVerify = true, customCertificatePath = null, logger = null, sharedCookieJar = null, sharedAxiosInstance = null ) { this._host = host.replace(/\/$/, ''); // Remove trailing slash this._username = username; this._password = password; this._useTlsVerify = useTlsVerify; this._customCertificatePath = customCertificatePath; this._maxRetries = 3; this.logger = logger || console; // Default to console if no logger provided // Use shared instances if provided, otherwise create new ones if (sharedCookieJar && sharedAxiosInstance) { this.cookieJar = sharedCookieJar; this.axiosInstance = sharedAxiosInstance; } else { // Initialize cookie jar for better cookie handling this.cookieJar = new CookieJar(); this.axiosInstance = wrapper(axios.create({ jar: this.cookieJar, withCredentials: true })); } } /** * Makes HTTP requests with automatic retry logic and error handling. * @param {string} method - HTTP method (GET, POST, etc.) * @param {string} url - Full URL to make the request to * @param {Object} [headers={}] - HTTP headers to include * @param {Object} [options={}] - Additional request options * @param {boolean} [skipCheck=false] - Whether to skip response validation * @returns {Promise<Object>} Promise resolving to axios response object * @throws {Error} Throws error if request fails after all retries */ async _requestWithRetry(method, url, headers = {}, options = {}, skipCheck = false) { this.logger.log(`${method.toUpperCase()} request to ${url}...`); for (let retry = 1; retry <= this._maxRetries; retry++) { try { const axiosConfig = { method: method.toLowerCase(), url: url, headers: { ...headers }, httpsAgent: this._customCertificatePath ? new (await import('https')).Agent({ ca: this._customCertificatePath }) : undefined, rejectUnauthorized: this._useTlsVerify, timeout: 30000, validateStatus: function (status) { return status < 500; // Accept any status less than 500 }, jar: this.cookieJar, withCredentials: true, ...options }; const response = await this.axiosInstance(axiosConfig); if (skipCheck) { return response; } await this._checkResponse(response); return response; } catch (error) { // Create a cleaner error object instead of dumping the whole axios response const cleanError = { message: error.message, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, code: error.code }; if (error.code === 'ECONNABORTED' || error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { if (retry === this._maxRetries) { const simpleError = new Error(`${error.code}: ${error.message}`); simpleError.originalError = cleanError; throw simpleError; } this.logger.warn( `Request to ${url} failed: ${error.message}, retry ${retry} of ${this._maxRetries}` ); await this._sleep(1000 * (retry + 1)); } else { const simpleError = new Error(`HTTP ${cleanError.status}: ${cleanError.message}`); simpleError.originalError = cleanError; throw simpleError; } } } throw new Error(`Max retries exceeded with no successful response to ${url}`); } /** * Validates the API response format and checks for success status. * @param {Object} response - Axios response object * @param {Object} response.data - Response data containing API fields * @returns {Promise<void>} Promise that resolves if response is valid * @throws {Error} Throws error if response indicates failure */ async _checkResponse(response) { const responseJson = response.data; const status = responseJson[ApiFields.SUCCESS]; const message = responseJson[ApiFields.MSG]; if (!status) { throw new Error(`Response status is not successful, message: ${message}`); } } /** * Utility method to pause execution for a specified duration. * @param {number} ms - Number of milliseconds to sleep * @returns {Promise<void>} Promise that resolves after the specified delay */ _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Constructs a complete URL by combining the host with an endpoint. * @param {string} endpoint - API endpoint path * @returns {string} Complete URL for the API request */ _url(endpoint) { return `${this._host}/${endpoint}`; } /** * Makes a POST request to the specified URL. * @param {string} url - URL to send the POST request to * @param {Object} [headers={}] - HTTP headers to include * @param {Object} [data={}] - Data to send in the request body * @param {Object} [options={}] - Additional request options * @returns {Promise<Object>} Promise resolving to axios response object * @throws {Error} Throws error if request fails */ async _post(url, headers = {}, data = {}, options = {}, skipCheck = false) { return this._requestWithRetry('post', url, headers, { data: data, ...options }, skipCheck); } /** * Makes a GET request to the specified URL. * @param {string} url - URL to send the GET request to * @param {Object} [headers={}] - HTTP headers to include * @param {Object} [options={}] - Additional request options * @returns {Promise<Object>} Promise resolving to axios response object * @throws {Error} Throws error if request fails */ async _get(url, headers = {}, options = {}, skipCheck = false) { return this._requestWithRetry('get', url, headers, { ...options }, skipCheck); } /** * Authenticates with the XUI API and establishes a session. * @param {string|null} [twoFactorCode=null] - Optional two-factor authentication code * @returns {Promise<void>} Promise that resolves when login is successful * @throws {Error} Throws error if login fails or no session cookie is received */ async login(twoFactorCode = null) { const endpoint = "login"; const headers = {}; const url = this._url(endpoint); const data = { username: this._username, password: this._password }; if (twoFactorCode !== null) { data.twoFactorCode = String(twoFactorCode); } this.logger.log(`Logging in with username: ${this._username}`); await this._post(url, headers, data); } } export default BaseApi;