- Refactored STARTTLS implementation to use Deno's native TLS via Deno.startTls(). - Introduced ConnectionWrapper to provide a Node.js net.Socket-compatible interface for Deno.Conn and Deno.TlsConn. - Updated TlsHandler to utilize the new STARTTLS implementation. - Added comprehensive SMTP authentication tests for PLAIN and LOGIN mechanisms. - Implemented rate limiting tests for SMTP server connections and commands. - Enhanced error handling and logging throughout the STARTTLS and connection upgrade processes.
299 lines
6.8 KiB
TypeScript
299 lines
6.8 KiB
TypeScript
/**
|
|
* Connection Wrapper Utility
|
|
* Wraps Deno.Conn to provide Node.js net.Socket-compatible interface
|
|
* This allows the SMTP server to use Deno's native networking while maintaining
|
|
* compatibility with existing Socket-based code
|
|
*/
|
|
|
|
import { EventEmitter } from '../../../../plugins.ts';
|
|
|
|
/**
|
|
* Wraps a Deno.Conn or Deno.TlsConn to provide a Node.js Socket-compatible interface
|
|
*/
|
|
export class ConnectionWrapper extends EventEmitter {
|
|
private conn: Deno.Conn | Deno.TlsConn;
|
|
private _destroyed = false;
|
|
private _reading = false;
|
|
private _remoteAddr: Deno.NetAddr;
|
|
private _localAddr: Deno.NetAddr;
|
|
|
|
constructor(conn: Deno.Conn | Deno.TlsConn) {
|
|
super();
|
|
this.conn = conn;
|
|
this._remoteAddr = conn.remoteAddr as Deno.NetAddr;
|
|
this._localAddr = conn.localAddr as Deno.NetAddr;
|
|
|
|
// Start reading from the connection
|
|
this._reading = true;
|
|
this._startReading();
|
|
}
|
|
|
|
/**
|
|
* Get remote address (Node.js net.Socket compatible)
|
|
*/
|
|
get remoteAddress(): string {
|
|
return this._remoteAddr.hostname;
|
|
}
|
|
|
|
/**
|
|
* Get remote port (Node.js net.Socket compatible)
|
|
*/
|
|
get remotePort(): number {
|
|
return this._remoteAddr.port;
|
|
}
|
|
|
|
/**
|
|
* Get local address (Node.js net.Socket compatible)
|
|
*/
|
|
get localAddress(): string {
|
|
return this._localAddr.hostname;
|
|
}
|
|
|
|
/**
|
|
* Get local port (Node.js net.Socket compatible)
|
|
*/
|
|
get localPort(): number {
|
|
return this._localAddr.port;
|
|
}
|
|
|
|
/**
|
|
* Check if connection is destroyed
|
|
*/
|
|
get destroyed(): boolean {
|
|
return this._destroyed;
|
|
}
|
|
|
|
/**
|
|
* Check ready state (Node.js compatible)
|
|
*/
|
|
get readyState(): string {
|
|
if (this._destroyed) {
|
|
return 'closed';
|
|
}
|
|
return 'open';
|
|
}
|
|
|
|
/**
|
|
* Check if writable (Node.js compatible)
|
|
*/
|
|
get writable(): boolean {
|
|
return !this._destroyed;
|
|
}
|
|
|
|
/**
|
|
* Check if this is a secure (TLS) connection
|
|
*/
|
|
get encrypted(): boolean {
|
|
return 'handshake' in this.conn; // TlsConn has handshake property
|
|
}
|
|
|
|
/**
|
|
* Write data to the connection (Node.js net.Socket compatible)
|
|
*/
|
|
write(data: string | Uint8Array, encoding?: string | ((err?: Error) => void), callback?: (err?: Error) => void): boolean {
|
|
// Handle overloaded signatures (encoding is optional)
|
|
if (typeof encoding === 'function') {
|
|
callback = encoding;
|
|
encoding = undefined;
|
|
}
|
|
|
|
if (this._destroyed) {
|
|
const error = new Error('Connection is destroyed');
|
|
if (callback) {
|
|
setTimeout(() => callback(error), 0);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const bytes = typeof data === 'string'
|
|
? new TextEncoder().encode(data)
|
|
: data;
|
|
|
|
// Use a promise-based approach that Node.js compatibility expects
|
|
// Write happens async but we return true immediately (buffered)
|
|
this.conn.write(bytes)
|
|
.then(() => {
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
if (callback) {
|
|
callback(error);
|
|
} else {
|
|
this.emit('error', error);
|
|
}
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* End the connection (Node.js net.Socket compatible)
|
|
*/
|
|
end(data?: string | Uint8Array, encoding?: string, callback?: () => void): void {
|
|
if (data) {
|
|
this.write(data, encoding, () => {
|
|
this.destroy();
|
|
if (callback) callback();
|
|
});
|
|
} else {
|
|
this.destroy();
|
|
if (callback) callback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy the connection (Node.js net.Socket compatible)
|
|
*/
|
|
destroy(error?: Error): void {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
this._destroyed = true;
|
|
this._reading = false;
|
|
|
|
try {
|
|
this.conn.close();
|
|
} catch (closeError) {
|
|
// Ignore close errors
|
|
}
|
|
|
|
if (error) {
|
|
this.emit('error', error);
|
|
}
|
|
|
|
this.emit('close', !!error);
|
|
}
|
|
|
|
/**
|
|
* Set TCP_NODELAY option (Node.js net.Socket compatible)
|
|
*/
|
|
setNoDelay(noDelay: boolean = true): this {
|
|
try {
|
|
// @ts-ignore - Deno.Conn has setNoDelay
|
|
if (typeof this.conn.setNoDelay === 'function') {
|
|
// @ts-ignore
|
|
this.conn.setNoDelay(noDelay);
|
|
}
|
|
} catch {
|
|
// Ignore if not supported
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set keep-alive option (Node.js net.Socket compatible)
|
|
*/
|
|
setKeepAlive(enable: boolean = true, initialDelay?: number): this {
|
|
try {
|
|
// @ts-ignore - Deno.Conn has setKeepAlive
|
|
if (typeof this.conn.setKeepAlive === 'function') {
|
|
// @ts-ignore
|
|
this.conn.setKeepAlive(enable);
|
|
}
|
|
} catch {
|
|
// Ignore if not supported
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set timeout (Node.js net.Socket compatible)
|
|
*/
|
|
setTimeout(timeout: number, callback?: () => void): this {
|
|
// Deno doesn't have built-in socket timeout, but we can implement it
|
|
// For now, just accept the call without error (most timeout handling is done elsewhere)
|
|
if (callback) {
|
|
// If callback provided, we could set up a timer, but for now just ignore
|
|
// The SMTP server handles timeouts at a higher level
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Pause reading from the connection
|
|
*/
|
|
pause(): this {
|
|
this._reading = false;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Resume reading from the connection
|
|
*/
|
|
resume(): this {
|
|
if (!this._reading && !this._destroyed) {
|
|
this._reading = true;
|
|
this._startReading();
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Get the underlying Deno.Conn
|
|
*/
|
|
getDenoConn(): Deno.Conn | Deno.TlsConn {
|
|
return this.conn;
|
|
}
|
|
|
|
/**
|
|
* Replace the underlying connection (for STARTTLS upgrade)
|
|
*/
|
|
replaceConnection(newConn: Deno.TlsConn): void {
|
|
this.conn = newConn;
|
|
this._remoteAddr = newConn.remoteAddr as Deno.NetAddr;
|
|
this._localAddr = newConn.localAddr as Deno.NetAddr;
|
|
|
|
// Restart reading from the new TLS connection
|
|
if (!this._destroyed) {
|
|
this._reading = true;
|
|
this._startReading();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to read data from the connection
|
|
*/
|
|
private async _startReading(): Promise<void> {
|
|
if (!this._reading || this._destroyed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const buffer = new Uint8Array(4096);
|
|
|
|
while (this._reading && !this._destroyed) {
|
|
const n = await this.conn.read(buffer);
|
|
|
|
if (n === null) {
|
|
// EOF
|
|
this._destroyed = true;
|
|
this.emit('end');
|
|
this.emit('close', false);
|
|
break;
|
|
}
|
|
|
|
const data = buffer.subarray(0, n);
|
|
this.emit('data', data);
|
|
}
|
|
} catch (error) {
|
|
if (!this._destroyed) {
|
|
this._destroyed = true;
|
|
this.emit('error', error instanceof Error ? error : new Error(String(error)));
|
|
this.emit('close', true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all listeners (cleanup helper)
|
|
*/
|
|
removeAllListeners(event?: string): this {
|
|
super.removeAllListeners(event);
|
|
return this;
|
|
}
|
|
}
|