proxy-connection
Version:
Proxy client with automatic connection management, health checking, and fetch-like API
270 lines • 11.8 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.proxyRequest = proxyRequest;
// Utility to make HTTP requests via SOCKS5 proxy
const socks_proxy_agent_1 = require("socks-proxy-agent");
const https_1 = __importDefault(require("https"));
const http_1 = __importDefault(require("http"));
const form_data_1 = __importDefault(require("form-data"));
const errorCodes_1 = require("./errorCodes");
async function proxyRequest(requestConfig, proxy) {
const { url, method = 'GET', headers = {}, body } = requestConfig;
if (!url) {
throw {
message: 'URL is required',
errorCode: errorCodes_1.errorCodes.REQUEST_BODY_ERROR,
error: new Error('URL is required'),
config: requestConfig,
proxy
};
}
const isHttps = url.startsWith('https://');
const agent = new socks_proxy_agent_1.SocksProxyAgent(`socks5://${proxy.user}:${proxy.pass}@${proxy.ip}:${proxy.port}`);
const lib = isHttps ? https_1.default : http_1.default;
// Handle FormData body
let requestBody;
const requestHeaders = { ...headers };
if (body instanceof FormData) {
// For FormData, we need to convert it to a format that can be sent via HTTP
try {
const formDataNode = new form_data_1.default();
let hasFiles = false;
for (const [key, value] of body.entries()) {
if (value instanceof File || (value && typeof value === 'object' && value.constructor.name === 'File')) {
hasFiles = true;
// Convert File to Buffer for form-data
const fileBuffer = Buffer.from(await value.arrayBuffer());
formDataNode.append(key, fileBuffer, {
filename: value.name,
contentType: value.type || 'application/octet-stream'
});
}
else {
formDataNode.append(key, String(value));
}
}
if (hasFiles) {
// Use multipart/form-data for file uploads
requestBody = formDataNode.getBuffer();
const formHeaders = formDataNode.getHeaders();
Object.assign(requestHeaders, formHeaders);
}
else {
// For simple form data, convert to application/x-www-form-urlencoded
const params = new URLSearchParams();
for (const [key, value] of body.entries()) {
params.append(key, String(value));
}
requestBody = params.toString();
requestHeaders['content-type'] = 'application/x-www-form-urlencoded';
}
if (requestBody) {
requestHeaders['content-length'] = Buffer.byteLength(requestBody).toString();
}
}
catch (error) {
throw {
message: 'FormData processing error',
errorCode: errorCodes_1.errorCodes.REQUEST_BODY_ERROR,
error: error,
config: requestConfig,
proxy
};
}
}
else if (body) {
// Handle other body types
if (typeof body === 'string') {
requestBody = body;
}
else if (Buffer.isBuffer(body)) {
requestBody = body;
}
else {
// Assume it's an object that needs JSON stringifying
requestBody = JSON.stringify(body);
if (!requestHeaders['content-type']) {
requestHeaders['content-type'] = 'application/json';
}
}
if (requestBody && !requestHeaders['content-length']) {
requestHeaders['content-length'] = Buffer.byteLength(requestBody).toString();
}
}
return new Promise((resolve, reject) => {
const req = lib.request(url, {
method,
headers: requestHeaders,
agent
}, (res) => {
let data = '';
const chunks = [];
res.on('data', (chunk) => {
chunks.push(chunk);
// Only append to data string if content is text-based
if (res.headers['content-type']?.includes('text') ||
res.headers['content-type']?.includes('json') ||
res.headers['content-type']?.includes('application/json')) {
data += chunk;
}
});
res.on('end', () => {
const buffer = Buffer.concat(chunks);
// For text content, use string data; for binary, use buffer
const isTextContent = res.headers['content-type']?.includes('text') ||
res.headers['content-type']?.includes('json') ||
res.headers['content-type']?.includes('application/json');
if (!isTextContent && !data) {
data = buffer.toString('utf8');
}
// Create ReadableStream from buffer
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(buffer));
controller.close();
}
});
// Create Headers-like object compatible with fetch API
const createHeaders = (nodeHeaders) => {
const headersMap = new Map();
for (const [key, value] of Object.entries(nodeHeaders)) {
if (value !== undefined) {
headersMap.set(key.toLowerCase(), Array.isArray(value) ? value.join(', ') : String(value));
}
}
return {
get: (name) => headersMap.get(name.toLowerCase()) || null,
has: (name) => headersMap.has(name.toLowerCase()),
entries: () => headersMap.entries(),
keys: () => headersMap.keys(),
values: () => headersMap.values(),
forEach: (callback) => headersMap.forEach(callback),
[Symbol.iterator]: () => headersMap.entries()
};
};
// fetch-like Response imitation
const response = {
ok: res.statusCode ? res.statusCode >= 200 && res.statusCode < 300 : false,
status: res.statusCode || 0,
statusText: res.statusMessage || '',
headers: createHeaders(res.headers),
url,
redirected: false,
type: 'default',
body: stream,
bodyUsed: false,
clone() {
if (response.bodyUsed) {
throw new TypeError('Body has already been consumed');
}
const clonedStream = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(buffer));
controller.close();
}
});
return {
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers: response.headers,
url: response.url,
redirected: response.redirected,
type: response.type,
body: clonedStream,
bodyUsed: false,
text: async () => data,
json: async () => { try {
return JSON.parse(data);
}
catch {
return data;
} },
arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
blob: async () => new Blob([buffer]),
formData: async () => { throw new Error('FormData not implemented'); },
clone: () => { throw new Error('Cannot clone already cloned response'); }
};
},
// fetch API methods
text: async () => {
if (response.bodyUsed) {
throw new TypeError('Body has already been consumed');
}
response.bodyUsed = true;
return data;
},
json: async () => {
if (response.bodyUsed) {
throw new TypeError('Body has already been consumed');
}
response.bodyUsed = true;
try {
return JSON.parse(data);
}
catch {
return data;
}
},
arrayBuffer: async () => {
if (response.bodyUsed) {
throw new TypeError('Body has already been consumed');
}
response.bodyUsed = true;
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
},
blob: async () => {
if (response.bodyUsed) {
throw new TypeError('Body has already been consumed');
}
response.bodyUsed = true;
return new Blob([buffer]);
},
formData: async () => {
throw new Error('FormData not implemented');
}
};
resolve(response);
agent.destroy();
});
});
req.on('error', (err) => {
reject({
message: errorCodes_1.errorMessages[errorCodes_1.errorCodes.REQUEST_FAILED],
errorCode: errorCodes_1.errorCodes.REQUEST_FAILED,
error: err,
proxy
});
agent.destroy();
});
req.setTimeout(10000, () => {
req.destroy();
reject({
message: errorCodes_1.errorMessages[errorCodes_1.errorCodes.REQUEST_TIMEOUT],
errorCode: errorCodes_1.errorCodes.REQUEST_TIMEOUT,
error: new Error('Request timed out'),
proxy
});
agent.destroy();
});
if (requestBody) {
try {
req.write(requestBody);
}
catch (err) {
reject({
message: errorCodes_1.errorMessages[errorCodes_1.errorCodes.REQUEST_BODY_ERROR],
errorCode: errorCodes_1.errorCodes.REQUEST_BODY_ERROR,
error: err,
proxy
});
agent.destroy();
}
}
req.end();
});
}
//# sourceMappingURL=proxyRequest.js.map