542 lines
18 KiB
JavaScript
542 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
const proxyquire = require('proxyquire').noCallThru();
|
|
|
|
// Mock socket object
|
|
const createMockSocket = () => ({
|
|
on: () => {},
|
|
write: () => {},
|
|
end: () => {},
|
|
destroy: () => {}
|
|
});
|
|
|
|
// Mock logger
|
|
const createMockLogger = () => {
|
|
const logs = { info: [], error: [] };
|
|
return {
|
|
info: msg => logs.info.push(msg),
|
|
error: msg => logs.error.push(msg),
|
|
_logs: logs
|
|
};
|
|
};
|
|
|
|
// ============================================
|
|
// HTTP Proxy Tests
|
|
// ============================================
|
|
|
|
module.exports['Proxy Connection: HTTP proxy success'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(null, mockSocket);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
|
|
|
|
test.equal(socket, mockSocket);
|
|
test.equal(logger._logs.info.length, 1);
|
|
test.ok(logger._logs.info[0].msg.includes('HTTP proxy'));
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: HTTPS proxy success'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(null, mockSocket);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
const socket = await proxyConnection(logger, 'https://proxy.example.com:8080', '192.168.1.1', 993);
|
|
|
|
test.equal(socket, mockSocket);
|
|
test.equal(logger._logs.info.length, 1);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: HTTP proxy with password hides it in logs'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(null, mockSocket);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'http://user:secret123@proxy.example.com:8080', '192.168.1.1', 993);
|
|
|
|
test.equal(logger._logs.info.length, 1);
|
|
test.ok(!logger._logs.info[0].proxyUrl.includes('secret123'));
|
|
test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: HTTP proxy failure'] = async test => {
|
|
const logger = createMockLogger();
|
|
const testError = new Error('Connection refused');
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(testError);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
try {
|
|
await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
|
|
test.ok(false, 'Should have thrown');
|
|
} catch (err) {
|
|
test.equal(err, testError);
|
|
test.equal(logger._logs.error.length, 1);
|
|
test.ok(logger._logs.error[0].msg.includes('Failed'));
|
|
}
|
|
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: HTTP proxy failure hides password'] = async test => {
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(new Error('Failed'));
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
try {
|
|
await proxyConnection(logger, 'http://user:secret@proxy.example.com:8080', '192.168.1.1', 993);
|
|
} catch (err) {
|
|
// Error expected - we just need to verify logging
|
|
err.expected = true;
|
|
test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
|
|
test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
|
|
}
|
|
|
|
test.done();
|
|
};
|
|
|
|
// ============================================
|
|
// SOCKS Proxy Tests
|
|
// ============================================
|
|
|
|
module.exports['Proxy Connection: SOCKS5 proxy success'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.type, 5);
|
|
test.equal(opts.command, 'connect');
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
const socket = await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
|
|
|
|
test.equal(socket, mockSocket);
|
|
test.equal(logger._logs.info.length, 1);
|
|
test.ok(logger._logs.info[0].msg.includes('SOCKS proxy'));
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS proxy (defaults to SOCKS5)'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.type, 5); // Default to SOCKS5
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks://proxy.example.com:1080', '192.168.1.1', 993);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS4 proxy'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.type, 4);
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks4://proxy.example.com:1080', '192.168.1.1', 993);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS4a proxy'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.type, 4);
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks4a://proxy.example.com:1080', '192.168.1.1', 993);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS proxy with authentication'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.userId, 'testuser');
|
|
test.equal(opts.proxy.password, 'testpass');
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks5://testuser:testpass@proxy.example.com:1080', '192.168.1.1', 993);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS proxy hides password in logs'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async () => ({ socket: mockSocket })
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks5://user:secretpass@proxy.example.com:1080', '192.168.1.1', 993);
|
|
|
|
test.ok(!logger._logs.info[0].proxyUrl.includes('secretpass'));
|
|
test.ok(logger._logs.info[0].proxyUrl.includes('(hidden)'));
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS proxy default port'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.port, 1080); // Default SOCKS port
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks5://proxy.example.com', '192.168.1.1', 993);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS proxy failure'] = async test => {
|
|
const logger = createMockLogger();
|
|
const testError = new Error('SOCKS connection failed');
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async () => {
|
|
throw testError;
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
try {
|
|
await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
|
|
test.ok(false, 'Should have thrown');
|
|
} catch (err) {
|
|
test.equal(err, testError);
|
|
test.equal(logger._logs.error.length, 1);
|
|
test.ok(logger._logs.error[0].msg.includes('Failed'));
|
|
}
|
|
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS proxy failure hides password'] = async test => {
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async () => {
|
|
throw new Error('Failed');
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
try {
|
|
await proxyConnection(logger, 'socks5://user:secret@proxy.example.com:1080', '192.168.1.1', 993);
|
|
} catch (err) {
|
|
// Error expected - we just need to verify logging
|
|
err.expected = true;
|
|
test.ok(!logger._logs.error[0].proxyUrl.includes('secret'));
|
|
test.ok(logger._logs.error[0].proxyUrl.includes('(hidden)'));
|
|
}
|
|
|
|
test.done();
|
|
};
|
|
|
|
// ============================================
|
|
// DNS Resolution Tests
|
|
// ============================================
|
|
|
|
module.exports['Proxy Connection: Resolves hostname to IP'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
let resolvedHost = null;
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
resolvedHost = host;
|
|
cb(null, mockSocket);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: {
|
|
promises: {
|
|
resolve: async hostname => {
|
|
if (hostname === 'mail.example.com') {
|
|
return ['93.184.216.34'];
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
},
|
|
net: { isIP: host => /^\d+\.\d+\.\d+\.\d+$/.test(host) }
|
|
});
|
|
|
|
await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
|
|
|
|
test.equal(resolvedHost, '93.184.216.34');
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: Skips DNS for IP addresses'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
let dnsResolveCalled = false;
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(null, mockSocket);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: {
|
|
promises: {
|
|
resolve: async () => {
|
|
dnsResolveCalled = true;
|
|
return ['127.0.0.1'];
|
|
}
|
|
}
|
|
},
|
|
net: { isIP: () => true } // Pretend it's already an IP
|
|
});
|
|
|
|
await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
|
|
|
|
test.equal(dnsResolveCalled, false);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS resolves proxy hostname'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
let proxyHostResolved = null;
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
proxyHostResolved = opts.proxy.host;
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: {
|
|
promises: {
|
|
resolve: async hostname => {
|
|
if (hostname === 'proxy.example.com') {
|
|
return ['10.0.0.1'];
|
|
}
|
|
return ['127.0.0.1'];
|
|
}
|
|
}
|
|
},
|
|
net: { isIP: host => /^\d+\.\d+\.\d+\.\d+$/.test(host) }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks5://proxy.example.com:1080', '192.168.1.1', 993);
|
|
|
|
test.equal(proxyHostResolved, '10.0.0.1');
|
|
test.done();
|
|
};
|
|
|
|
// ============================================
|
|
// Edge Cases
|
|
// ============================================
|
|
|
|
module.exports['Proxy Connection: Unknown protocol returns undefined'] = async test => {
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
const result = await proxyConnection(logger, 'ftp://proxy.example.com:21', '192.168.1.1', 993);
|
|
|
|
test.equal(result, undefined);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: HTTP proxy with no socket returned'] = async test => {
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
cb(null, null); // No socket
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
const socket = await proxyConnection(logger, 'http://proxy.example.com:8080', '192.168.1.1', 993);
|
|
|
|
test.equal(socket, null);
|
|
// No log when socket is null
|
|
test.equal(logger._logs.info.length, 0);
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: DNS returns empty result'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
let usedHost = null;
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': (url, port, host, cb) => {
|
|
usedHost = host;
|
|
cb(null, mockSocket);
|
|
},
|
|
socks: { SocksClient: {} },
|
|
dns: {
|
|
promises: {
|
|
resolve: async () => [] // Empty result
|
|
}
|
|
},
|
|
net: { isIP: () => false }
|
|
});
|
|
|
|
await proxyConnection(logger, 'http://proxy.example.com:8080', 'mail.example.com', 993);
|
|
|
|
// Should keep original hostname when DNS returns empty
|
|
test.equal(usedHost, 'mail.example.com');
|
|
test.done();
|
|
};
|
|
|
|
module.exports['Proxy Connection: SOCKS with username only'] = async test => {
|
|
const mockSocket = createMockSocket();
|
|
const logger = createMockLogger();
|
|
|
|
const { proxyConnection } = proxyquire('../lib/proxy-connection', {
|
|
'nodemailer/lib/smtp-connection/http-proxy-client': () => {},
|
|
socks: {
|
|
SocksClient: {
|
|
createConnection: async opts => {
|
|
test.equal(opts.proxy.userId, 'testuser');
|
|
test.equal(opts.proxy.password, ''); // Empty string from URL parsing
|
|
return { socket: mockSocket };
|
|
}
|
|
}
|
|
},
|
|
dns: { promises: { resolve: async () => ['127.0.0.1'] } },
|
|
net: { isIP: () => true }
|
|
});
|
|
|
|
await proxyConnection(logger, 'socks5://testuser@proxy.example.com:1080', '192.168.1.1', 993);
|
|
test.done();
|
|
};
|