mirror of
https://gitea.shironeko-all.duckdns.org/shironeko/Hytale-F2P-2.git
synced 2026-02-26 02:31:46 -03:00
Add smart proxy with direct-fallback and logging
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { downloadFile, findHomePageUIPath, findLogoPath } = require('../utils/fileManager');
|
||||
const { proxyRequest } = require('../utils/proxyClient');
|
||||
const { smartRequest } = require('../utils/proxyClient');
|
||||
|
||||
async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
|
||||
try {
|
||||
@@ -14,7 +14,7 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) {
|
||||
const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI';
|
||||
const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui');
|
||||
|
||||
const response = await proxyRequest(homeUIUrl, { responseType: 'arraybuffer' });
|
||||
const response = await smartRequest(homeUIUrl, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(tempHomePath, response.data);
|
||||
|
||||
const existingHomePath = findHomePageUIPath(gameDir);
|
||||
@@ -68,7 +68,7 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) {
|
||||
const logoUrl = 'https://files.hytalef2p.com/api/Logo';
|
||||
const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png');
|
||||
|
||||
const response = await proxyRequest(logoUrl, { responseType: 'arraybuffer' });
|
||||
const response = await smartRequest(logoUrl, { responseType: 'arraybuffer' });
|
||||
fs.writeFileSync(tempLogoPath, response.data);
|
||||
|
||||
const existingLogoPath = findLogoPath(gameDir);
|
||||
|
||||
@@ -2,7 +2,7 @@ const axios = require('axios');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
const { proxyRequest } = require('../utils/proxyClient');
|
||||
const { smartRequest } = require('../utils/proxyClient');
|
||||
|
||||
const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches';
|
||||
const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
|
||||
@@ -10,7 +10,7 @@ const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest';
|
||||
async function getLatestClientVersion(branch = 'release') {
|
||||
try {
|
||||
console.log(`Fetching latest client version from API (branch: ${branch})...`);
|
||||
const response = await proxyRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
|
||||
const response = await smartRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, {
|
||||
timeout: 40000,
|
||||
headers: {
|
||||
'User-Agent': 'Hytale-F2P-Launcher'
|
||||
@@ -66,7 +66,7 @@ async function fetchPatchManifest(branch = 'release') {
|
||||
try {
|
||||
const os = getOS();
|
||||
const arch = getArch();
|
||||
const response = await proxyRequest(`${MANIFEST_API}?branch=${branch}&os=${os}&arch=${arch}`, {
|
||||
const response = await smartRequest(`${MANIFEST_API}?branch=${branch}&os=${os}&arch=${arch}`, {
|
||||
timeout: 10000
|
||||
});
|
||||
return response.data.patches || {};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getProxyDownloadStream } = require('./proxyClient');
|
||||
const { smartDownloadStream } = require('./proxyClient');
|
||||
|
||||
// Domain configuration
|
||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||
@@ -619,7 +619,7 @@ class ClientPatcher {
|
||||
let totalSize = 0;
|
||||
let downloaded = 0;
|
||||
|
||||
const stream = await getProxyDownloadStream(url, (chunk, downloadedBytes, total) => {
|
||||
const stream = await smartDownloadStream(url, (chunk, downloadedBytes, total) => {
|
||||
downloaded = downloadedBytes;
|
||||
totalSize = total;
|
||||
if (progressCallback && totalSize) {
|
||||
|
||||
@@ -5,6 +5,13 @@ const { PassThrough } = require('stream');
|
||||
|
||||
const PROXY_URL = process.env.HF2P_PROXY_URL || 'your_proxy_url_here';
|
||||
const SECRET_KEY = process.env.HF2P_SECRET_KEY || 'your_secret_key_here_for_jwt';
|
||||
const USE_DIRECT_FALLBACK = process.env.HF2P_USE_FALLBACK !== 'false';
|
||||
const DIRECT_TIMEOUT = 7000; // 7 seconds timeout
|
||||
|
||||
console.log('[ProxyClient] Initialized with proxy URL:', PROXY_URL);
|
||||
console.log('[ProxyClient] Secret key configured:', SECRET_KEY ? 'YES' : 'NO');
|
||||
console.log('[ProxyClient] Direct connection fallback:', USE_DIRECT_FALLBACK ? 'ENABLED' : 'DISABLED');
|
||||
console.log('[ProxyClient] Direct timeout before fallback:', DIRECT_TIMEOUT / 1000, 'seconds');
|
||||
|
||||
function generateToken() {
|
||||
const timestamp = Date.now().toString();
|
||||
@@ -12,17 +19,67 @@ function generateToken() {
|
||||
.createHmac('sha256', SECRET_KEY)
|
||||
.update(timestamp)
|
||||
.digest('hex');
|
||||
return `${timestamp}:${hash}`;
|
||||
const token = `${timestamp}:${hash}`;
|
||||
console.log('[ProxyClient] Generated auth token:', token.substring(0, 20) + '...');
|
||||
return token;
|
||||
}
|
||||
|
||||
// Direct request without proxy
|
||||
async function directRequest(url, options = {}) {
|
||||
console.log('[ProxyClient] Attempting direct request (no proxy)');
|
||||
console.log('[ProxyClient] Direct URL:', url);
|
||||
|
||||
const timeoutMs = options.timeout || DIRECT_TIMEOUT;
|
||||
const controller = new AbortController();
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn('[ProxyClient] ⏱️ TIMEOUT! Aborting direct request after', timeoutMs, 'ms');
|
||||
controller.abort();
|
||||
}, timeoutMs);
|
||||
|
||||
try {
|
||||
const config = {
|
||||
method: options.method || 'GET',
|
||||
url: url,
|
||||
headers: options.headers || {},
|
||||
timeout: timeoutMs,
|
||||
responseType: options.responseType,
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
const response = await axios(config);
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy request (original function)
|
||||
async function proxyRequest(url, options = {}) {
|
||||
console.log('[ProxyClient] Starting proxy request');
|
||||
console.log('[ProxyClient] Original URL:', url);
|
||||
console.log('[ProxyClient] Options:', JSON.stringify(options, null, 2));
|
||||
|
||||
try {
|
||||
const token = generateToken();
|
||||
const urlObj = new URL(url);
|
||||
const targetUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||
|
||||
console.log('[ProxyClient] Parsed URL components:');
|
||||
console.log(' - Protocol:', urlObj.protocol);
|
||||
console.log(' - Host:', urlObj.host);
|
||||
console.log(' - Pathname:', urlObj.pathname);
|
||||
console.log(' - Search:', urlObj.search);
|
||||
console.log(' - Target URL:', targetUrl);
|
||||
|
||||
const proxyEndpoint = `${PROXY_URL}/proxy${urlObj.pathname}${urlObj.search}`;
|
||||
console.log('[ProxyClient] Proxy endpoint:', proxyEndpoint);
|
||||
|
||||
const config = {
|
||||
method: options.method || 'GET',
|
||||
url: `${PROXY_URL}/proxy${urlObj.pathname}${urlObj.search}`,
|
||||
url: proxyEndpoint,
|
||||
headers: {
|
||||
'X-Auth-Token': token,
|
||||
'X-Target-URL': targetUrl,
|
||||
@@ -32,51 +89,117 @@ async function proxyRequest(url, options = {}) {
|
||||
responseType: options.responseType
|
||||
};
|
||||
|
||||
return axios(config);
|
||||
console.log('[ProxyClient] Request config:', JSON.stringify({
|
||||
method: config.method,
|
||||
url: config.url,
|
||||
headers: config.headers,
|
||||
timeout: config.timeout,
|
||||
responseType: config.responseType
|
||||
}, null, 2));
|
||||
|
||||
const response = await axios(config);
|
||||
console.log('[ProxyClient] Response received - Status:', response.status);
|
||||
console.log('[ProxyClient] Response headers:', JSON.stringify(response.headers, null, 2));
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('[ProxyClient] Request failed!');
|
||||
console.error('[ProxyClient] Error type:', error.constructor.name);
|
||||
console.error('[ProxyClient] Error message:', error.message);
|
||||
if (error.response) {
|
||||
console.error('[ProxyClient] Response status:', error.response.status);
|
||||
console.error('[ProxyClient] Response data:', error.response.data);
|
||||
console.error('[ProxyClient] Response headers:', error.response.headers);
|
||||
}
|
||||
if (error.config) {
|
||||
console.error('[ProxyClient] Failed request URL:', error.config.url);
|
||||
console.error('[ProxyClient] Failed request headers:', error.config.headers);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function getProxyDownloadStream(url, onData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const token = generateToken();
|
||||
const urlObj = new URL(url);
|
||||
const targetUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||
// Smart request with automatic fallback
|
||||
async function smartRequest(url, options = {}) {
|
||||
if (!USE_DIRECT_FALLBACK) {
|
||||
console.log('[ProxyClient] Fallback disabled, using proxy directly');
|
||||
return proxyRequest(url, options);
|
||||
}
|
||||
|
||||
const proxyUrl = new URL(PROXY_URL);
|
||||
const requestPath = `/proxy${urlObj.pathname}${urlObj.search}`;
|
||||
console.log('[ProxyClient] Smart request with fallback enabled');
|
||||
console.log('[ProxyClient] Direct timeout configured:', DIRECT_TIMEOUT, 'ms');
|
||||
|
||||
const directStartTime = Date.now();
|
||||
try {
|
||||
console.log('[ProxyClient] [ATTEMPT 1/2] Trying direct connection first...');
|
||||
const response = await directRequest(url, options);
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Direct connection successful in', directDuration, 'ms');
|
||||
return response;
|
||||
} catch (directError) {
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.warn('[ProxyClient] [FAILED] Direct connection failed after', directDuration, 'ms');
|
||||
console.warn('[ProxyClient] Error message:', directError.message);
|
||||
console.warn('[ProxyClient] Error code:', directError.code);
|
||||
|
||||
// Always fallback to proxy on any error
|
||||
console.log('[ProxyClient] Attempting proxy fallback for all errors...');
|
||||
|
||||
if (true) {
|
||||
console.log('[ProxyClient] [ATTEMPT 2/2] Falling back to proxy connection...');
|
||||
try {
|
||||
const proxyStartTime = Date.now();
|
||||
const response = await proxyRequest(url, options);
|
||||
const proxyDuration = Date.now() - proxyStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Proxy connection successful in', proxyDuration, 'ms');
|
||||
return response;
|
||||
} catch (proxyError) {
|
||||
console.error('[ProxyClient] [FAILED] Both direct and proxy connections failed!');
|
||||
console.error('[ProxyClient] Direct error:', directError.message);
|
||||
console.error('[ProxyClient] Proxy error:', proxyError.message);
|
||||
throw proxyError;
|
||||
}
|
||||
} else {
|
||||
console.log('[ProxyClient] [SKIP] Direct error not related to connectivity, not falling back');
|
||||
throw directError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Direct download stream without proxy
|
||||
function directDownloadStream(url, onData) {
|
||||
console.log('[ProxyClient] Starting direct download stream (no proxy)');
|
||||
console.log('[ProxyClient] Direct download URL:', url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const protocol = urlObj.protocol === 'https:' ? https : require('http');
|
||||
|
||||
const options = {
|
||||
hostname: proxyUrl.hostname,
|
||||
port: proxyUrl.port || (proxyUrl.protocol === 'https:' ? 443 : 80),
|
||||
path: requestPath,
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
||||
path: urlObj.pathname + urlObj.search,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': token,
|
||||
'X-Target-URL': targetUrl
|
||||
}
|
||||
timeout: DIRECT_TIMEOUT
|
||||
};
|
||||
|
||||
const protocol = proxyUrl.protocol === 'https:' ? https : require('http');
|
||||
|
||||
const handleResponse = (response) => {
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
const redirectUrl = response.headers.location;
|
||||
if (redirectUrl.startsWith('http')) {
|
||||
getProxyDownloadStream(redirectUrl, onData).then(resolve).catch(reject);
|
||||
} else {
|
||||
reject(new Error(`Invalid redirect: ${redirectUrl}`));
|
||||
}
|
||||
console.log('[ProxyClient] Direct redirect to:', redirectUrl);
|
||||
directDownloadStream(redirectUrl, onData).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
reject(new Error(`Direct HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (onData) {
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
let downloaded = 0;
|
||||
|
||||
const passThrough = new PassThrough();
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
@@ -91,12 +214,213 @@ function getProxyDownloadStream(url, onData) {
|
||||
}
|
||||
};
|
||||
|
||||
protocol.get(options, handleResponse).on('error', reject);
|
||||
const req = protocol.get(options, handleResponse);
|
||||
|
||||
req.on('error', (error) => {
|
||||
console.error('[ProxyClient] Direct download error:', error.message);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
console.warn('[ProxyClient] ⏱️ TIMEOUT! Direct download timed out after', DIRECT_TIMEOUT, 'ms');
|
||||
req.destroy();
|
||||
const timeoutError = new Error('ETIMEDOUT: Direct connection timeout');
|
||||
timeoutError.code = 'ETIMEDOUT';
|
||||
reject(timeoutError);
|
||||
});
|
||||
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getProxyDownloadStream(url, onData) {
|
||||
console.log('[ProxyClient] Starting download stream');
|
||||
console.log('[ProxyClient] Download URL:', url);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const token = generateToken();
|
||||
const urlObj = new URL(url);
|
||||
const targetUrl = `${urlObj.protocol}//${urlObj.host}`;
|
||||
|
||||
console.log('[ProxyClient] Download URL parsed:');
|
||||
console.log(' - Protocol:', urlObj.protocol);
|
||||
console.log(' - Host:', urlObj.host);
|
||||
console.log(' - Hostname:', urlObj.hostname);
|
||||
console.log(' - Port:', urlObj.port);
|
||||
console.log(' - Pathname:', urlObj.pathname);
|
||||
console.log(' - Search:', urlObj.search);
|
||||
console.log(' - Target URL:', targetUrl);
|
||||
|
||||
const proxyUrl = new URL(PROXY_URL);
|
||||
const requestPath = `/proxy${urlObj.pathname}${urlObj.search}`;
|
||||
|
||||
console.log('[ProxyClient] Proxy configuration:');
|
||||
console.log(' - Proxy URL:', PROXY_URL);
|
||||
console.log(' - Proxy protocol:', proxyUrl.protocol);
|
||||
console.log(' - Proxy hostname:', proxyUrl.hostname);
|
||||
console.log(' - Proxy port:', proxyUrl.port);
|
||||
console.log(' - Request path:', requestPath);
|
||||
|
||||
const options = {
|
||||
hostname: proxyUrl.hostname,
|
||||
port: proxyUrl.port || (proxyUrl.protocol === 'https:' ? 443 : 80),
|
||||
path: requestPath,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Token': token,
|
||||
'X-Target-URL': targetUrl
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[ProxyClient] HTTP request options:', JSON.stringify(options, null, 2));
|
||||
|
||||
const protocol = proxyUrl.protocol === 'https:' ? https : require('http');
|
||||
console.log('[ProxyClient] Using protocol:', proxyUrl.protocol);
|
||||
|
||||
const handleResponse = (response) => {
|
||||
console.log('[ProxyClient] Response received - Status:', response.statusCode);
|
||||
console.log('[ProxyClient] Response headers:', JSON.stringify(response.headers, null, 2));
|
||||
|
||||
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||
const redirectUrl = response.headers.location;
|
||||
console.log('[ProxyClient] Redirect detected to:', redirectUrl);
|
||||
|
||||
if (redirectUrl.startsWith('http')) {
|
||||
console.log('[ProxyClient] Following redirect...');
|
||||
getProxyDownloadStream(redirectUrl, onData).then(resolve).catch(reject);
|
||||
} else {
|
||||
console.error('[ProxyClient] Invalid redirect URL:', redirectUrl);
|
||||
reject(new Error(`Invalid redirect: ${redirectUrl}`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
console.error('[ProxyClient] Unexpected status code:', response.statusCode);
|
||||
console.error('[ProxyClient] Response message:', response.statusMessage);
|
||||
reject(new Error(`HTTP ${response.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (onData) {
|
||||
const totalSize = parseInt(response.headers['content-length'], 10);
|
||||
console.log('[ProxyClient] Download starting - Total size:', totalSize, 'bytes');
|
||||
|
||||
let downloaded = 0;
|
||||
const passThrough = new PassThrough();
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
downloaded += chunk.length;
|
||||
const progress = ((downloaded / totalSize) * 100).toFixed(2);
|
||||
onData(chunk, downloaded, totalSize);
|
||||
});
|
||||
|
||||
response.on('end', () => {
|
||||
console.log('[ProxyClient] Download completed -', downloaded, 'bytes received');
|
||||
});
|
||||
|
||||
response.on('error', (error) => {
|
||||
console.error('[ProxyClient] Response stream error:', error.message);
|
||||
});
|
||||
|
||||
response.pipe(passThrough);
|
||||
console.log('[ProxyClient] Stream piped to PassThrough');
|
||||
resolve(passThrough);
|
||||
} else {
|
||||
console.log('[ProxyClient] Returning raw response stream (no progress callback)');
|
||||
resolve(response);
|
||||
}
|
||||
};
|
||||
|
||||
const request = protocol.get(options, handleResponse);
|
||||
|
||||
request.on('error', (error) => {
|
||||
console.error('[ProxyClient] HTTP request error!');
|
||||
console.error('[ProxyClient] Error type:', error.constructor.name);
|
||||
console.error('[ProxyClient] Error message:', error.message);
|
||||
console.error('[ProxyClient] Error code:', error.code);
|
||||
console.error('[ProxyClient] Error stack:', error.stack);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
console.log('[ProxyClient] HTTP request sent');
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ProxyClient] Exception in getProxyDownloadStream!');
|
||||
console.error('[ProxyClient] Error type:', error.constructor.name);
|
||||
console.error('[ProxyClient] Error message:', error.message);
|
||||
console.error('[ProxyClient] Error stack:', error.stack);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Smart download stream with automatic fallback
|
||||
function smartDownloadStream(url, onData) {
|
||||
if (!USE_DIRECT_FALLBACK) {
|
||||
console.log('[ProxyClient] Fallback disabled, using proxy stream directly');
|
||||
return getProxyDownloadStream(url, onData);
|
||||
}
|
||||
|
||||
console.log('[ProxyClient] Smart download stream with fallback enabled');
|
||||
console.log('[ProxyClient] Direct timeout configured:', DIRECT_TIMEOUT, 'ms');
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const directStartTime = Date.now();
|
||||
try {
|
||||
console.log('[ProxyClient] [DOWNLOAD 1/2] Trying direct download first...');
|
||||
const stream = await directDownloadStream(url, onData);
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Direct download stream established in', directDuration, 'ms');
|
||||
resolve(stream);
|
||||
} catch (directError) {
|
||||
const directDuration = Date.now() - directStartTime;
|
||||
console.warn('[ProxyClient] [FAILED] Direct download failed after', directDuration, 'ms');
|
||||
console.warn('[ProxyClient] Error message:', directError.message);
|
||||
console.warn('[ProxyClient] Error code:', directError.code);
|
||||
|
||||
// Always fallback to proxy on any error
|
||||
console.log('[ProxyClient] Attempting proxy fallback for all download errors...');
|
||||
|
||||
if (true) {
|
||||
console.log('[ProxyClient] [DOWNLOAD 2/2] Falling back to proxy download...');
|
||||
try {
|
||||
const proxyStartTime = Date.now();
|
||||
const stream = await getProxyDownloadStream(url, onData);
|
||||
const proxyDuration = Date.now() - proxyStartTime;
|
||||
console.log('[ProxyClient] [SUCCESS] Proxy download stream established in', proxyDuration, 'ms');
|
||||
resolve(stream);
|
||||
} catch (proxyError) {
|
||||
console.error('[ProxyClient] [FAILED] Both direct and proxy downloads failed!');
|
||||
console.error('[ProxyClient] Direct error:', directError.message);
|
||||
console.error('[ProxyClient] Proxy error:', proxyError.message);
|
||||
reject(proxyError);
|
||||
}
|
||||
} else {
|
||||
console.log('[ProxyClient] [SKIP] Direct error not related to connectivity, not falling back');
|
||||
reject(directError);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// Recommended: Smart functions with automatic fallback
|
||||
smartRequest,
|
||||
smartDownloadStream,
|
||||
|
||||
// Legacy: Direct proxy functions (for manual control)
|
||||
proxyRequest,
|
||||
getProxyDownloadStream,
|
||||
|
||||
// Direct functions (no proxy)
|
||||
directRequest,
|
||||
directDownloadStream,
|
||||
|
||||
// Utilities
|
||||
generateToken
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user