update
This commit is contained in:
298
test/suite/smtpclient_connection/test.ccm-10.proxy-support.ts
Normal file
298
test/suite/smtpclient_connection/test.ccm-10.proxy-support.ts
Normal file
@ -0,0 +1,298 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import * as net from 'net';
|
||||
import * as http from 'http';
|
||||
|
||||
let testServer: any;
|
||||
let proxyServer: http.Server;
|
||||
let socksProxyServer: net.Server;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Setup HTTP CONNECT proxy', async () => {
|
||||
// Create a simple HTTP CONNECT proxy
|
||||
proxyServer = http.createServer();
|
||||
|
||||
proxyServer.on('connect', (req, clientSocket, head) => {
|
||||
console.log(`Proxy CONNECT request to ${req.url}`);
|
||||
|
||||
const [host, port] = req.url!.split(':');
|
||||
const serverSocket = net.connect(parseInt(port), host, () => {
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
|
||||
'Proxy-agent: Test-Proxy\r\n' +
|
||||
'\r\n');
|
||||
|
||||
// Pipe data between client and server
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
});
|
||||
|
||||
serverSocket.on('error', (err) => {
|
||||
console.error('Proxy server socket error:', err);
|
||||
clientSocket.end();
|
||||
});
|
||||
|
||||
clientSocket.on('error', (err) => {
|
||||
console.error('Proxy client socket error:', err);
|
||||
serverSocket.end();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
proxyServer.listen(0, '127.0.0.1', () => {
|
||||
const address = proxyServer.address() as net.AddressInfo;
|
||||
console.log(`HTTP proxy listening on port ${address.port}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Test connection through HTTP proxy', async () => {
|
||||
const proxyAddress = proxyServer.address() as net.AddressInfo;
|
||||
|
||||
// Note: Real SMTP clients would need proxy configuration
|
||||
// This simulates what a proxy-aware SMTP client would do
|
||||
const proxyOptions = {
|
||||
host: proxyAddress.address,
|
||||
port: proxyAddress.port,
|
||||
method: 'CONNECT',
|
||||
path: `127.0.0.1:${testServer.port}`,
|
||||
headers: {
|
||||
'Proxy-Authorization': 'Basic dGVzdDp0ZXN0' // test:test in base64
|
||||
}
|
||||
};
|
||||
|
||||
const connected = await new Promise<boolean>((resolve) => {
|
||||
const req = http.request(proxyOptions);
|
||||
|
||||
req.on('connect', (res, socket, head) => {
|
||||
console.log('Connected through proxy, status:', res.statusCode);
|
||||
expect(res.statusCode).toEqual(200);
|
||||
|
||||
// Now we have a raw socket to the SMTP server through the proxy
|
||||
socket.on('data', (data) => {
|
||||
const response = data.toString();
|
||||
console.log('SMTP response through proxy:', response.trim());
|
||||
if (response.includes('220')) {
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.error('Proxy request error:', err);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(connected).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Test SOCKS5 proxy simulation', async () => {
|
||||
// Create a minimal SOCKS5 proxy for testing
|
||||
socksProxyServer = net.createServer((clientSocket) => {
|
||||
let authenticated = false;
|
||||
let targetHost: string;
|
||||
let targetPort: number;
|
||||
|
||||
clientSocket.on('data', (data) => {
|
||||
if (!authenticated) {
|
||||
// SOCKS5 handshake
|
||||
if (data[0] === 0x05) { // SOCKS version 5
|
||||
// Send back: no authentication required
|
||||
clientSocket.write(Buffer.from([0x05, 0x00]));
|
||||
authenticated = true;
|
||||
}
|
||||
} else if (!targetHost) {
|
||||
// Connection request
|
||||
if (data[0] === 0x05 && data[1] === 0x01) { // CONNECT command
|
||||
const addressType = data[3];
|
||||
|
||||
if (addressType === 0x01) { // IPv4
|
||||
targetHost = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
|
||||
targetPort = (data[8] << 8) + data[9];
|
||||
|
||||
// Connect to target
|
||||
const serverSocket = net.connect(targetPort, targetHost, () => {
|
||||
// Send success response
|
||||
const response = Buffer.alloc(10);
|
||||
response[0] = 0x05; // SOCKS version
|
||||
response[1] = 0x00; // Success
|
||||
response[2] = 0x00; // Reserved
|
||||
response[3] = 0x01; // IPv4
|
||||
response[4] = data[4]; // Copy address
|
||||
response[5] = data[5];
|
||||
response[6] = data[6];
|
||||
response[7] = data[7];
|
||||
response[8] = data[8]; // Copy port
|
||||
response[9] = data[9];
|
||||
|
||||
clientSocket.write(response);
|
||||
|
||||
// Start proxying
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
});
|
||||
|
||||
serverSocket.on('error', (err) => {
|
||||
console.error('SOCKS target connection error:', err);
|
||||
clientSocket.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clientSocket.on('error', (err) => {
|
||||
console.error('SOCKS client error:', err);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
socksProxyServer.listen(0, '127.0.0.1', () => {
|
||||
const address = socksProxyServer.address() as net.AddressInfo;
|
||||
console.log(`SOCKS5 proxy listening on port ${address.port}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Test connection through SOCKS proxy
|
||||
const socksAddress = socksProxyServer.address() as net.AddressInfo;
|
||||
const socksClient = net.connect(socksAddress.port, socksAddress.address);
|
||||
|
||||
const connected = await new Promise<boolean>((resolve) => {
|
||||
let phase = 'handshake';
|
||||
|
||||
socksClient.on('connect', () => {
|
||||
// Send SOCKS5 handshake
|
||||
socksClient.write(Buffer.from([0x05, 0x01, 0x00])); // Version 5, 1 method, no auth
|
||||
});
|
||||
|
||||
socksClient.on('data', (data) => {
|
||||
if (phase === 'handshake' && data[0] === 0x05 && data[1] === 0x00) {
|
||||
phase = 'connect';
|
||||
// Send connection request
|
||||
const connectReq = Buffer.alloc(10);
|
||||
connectReq[0] = 0x05; // SOCKS version
|
||||
connectReq[1] = 0x01; // CONNECT
|
||||
connectReq[2] = 0x00; // Reserved
|
||||
connectReq[3] = 0x01; // IPv4
|
||||
connectReq[4] = 127; // 127.0.0.1
|
||||
connectReq[5] = 0;
|
||||
connectReq[6] = 0;
|
||||
connectReq[7] = 1;
|
||||
connectReq[8] = (testServer.port >> 8) & 0xFF; // Port high byte
|
||||
connectReq[9] = testServer.port & 0xFF; // Port low byte
|
||||
|
||||
socksClient.write(connectReq);
|
||||
} else if (phase === 'connect' && data[0] === 0x05 && data[1] === 0x00) {
|
||||
phase = 'connected';
|
||||
console.log('Connected through SOCKS5 proxy');
|
||||
// Now we're connected to the SMTP server
|
||||
} else if (phase === 'connected') {
|
||||
const response = data.toString();
|
||||
console.log('SMTP response through SOCKS:', response.trim());
|
||||
if (response.includes('220')) {
|
||||
socksClient.write('QUIT\r\n');
|
||||
socksClient.end();
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socksClient.on('error', (err) => {
|
||||
console.error('SOCKS client error:', err);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
setTimeout(() => resolve(false), 5000); // Timeout after 5 seconds
|
||||
});
|
||||
|
||||
expect(connected).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Test proxy authentication failure', async () => {
|
||||
// Create a proxy that requires authentication
|
||||
const authProxyServer = http.createServer();
|
||||
|
||||
authProxyServer.on('connect', (req, clientSocket, head) => {
|
||||
const authHeader = req.headers['proxy-authorization'];
|
||||
|
||||
if (!authHeader || authHeader !== 'Basic dGVzdDp0ZXN0') {
|
||||
clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n' +
|
||||
'Proxy-Authenticate: Basic realm="Test Proxy"\r\n' +
|
||||
'\r\n');
|
||||
clientSocket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Authentication successful, proceed with connection
|
||||
const [host, port] = req.url!.split(':');
|
||||
const serverSocket = net.connect(parseInt(port), host, () => {
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
authProxyServer.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const authProxyAddress = authProxyServer.address() as net.AddressInfo;
|
||||
|
||||
// Test without authentication
|
||||
const failedAuth = await new Promise<boolean>((resolve) => {
|
||||
const req = http.request({
|
||||
host: authProxyAddress.address,
|
||||
port: authProxyAddress.port,
|
||||
method: 'CONNECT',
|
||||
path: `127.0.0.1:${testServer.port}`
|
||||
});
|
||||
|
||||
req.on('connect', () => resolve(false));
|
||||
req.on('response', (res) => {
|
||||
expect(res.statusCode).toEqual(407);
|
||||
resolve(true);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(failedAuth).toBeTruthy();
|
||||
|
||||
authProxyServer.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test servers', async () => {
|
||||
if (proxyServer) {
|
||||
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
|
||||
}
|
||||
|
||||
if (socksProxyServer) {
|
||||
await new Promise<void>((resolve) => socksProxyServer.close(() => resolve()));
|
||||
}
|
||||
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user