class WTVFTP { wtvshared = null; wtvmime = null; minisrv_config = null; sendToClient = null; request_headers = null; ftp = null; url = null; constructor(minisrv_config, service_name, wtvshared, sendToClient, wtvmime) { this.minisrv_config = minisrv_config; this.sendToClient = sendToClient; this.wtvshared = wtvshared; this.wtvmime = wtvmime; this.url = require('url'); this.ftp = require('ftp'); this.dns = require('dns'); } handleFTPRequest(socket, request_headers) { this.request_headers = request_headers; const ftpUrl = request_headers.request_url; const parsed = this.url.parse(ftpUrl); let user = null; let pass = null; const host = parsed.hostname; if (parsed.auth) { const [username, password] = parsed.auth.split(':'); user = username; pass = password || null; } if (!user && !pass) { user = "anonymous"; pass = "anonymous@eff.org"; } const ftpClient = new this.ftp(); const port = parsed.port ? parseInt(parsed.port, 10) : 21; const path = decodeURIComponent(parsed.pathname || '/'); let dir = path; let filename = null; if (path && path !== '/') { const parts = path.split('/'); if (parts[parts.length - 1] && !path.endsWith('/')) { filename = parts.pop(); dir = parts.join('/') || '/'; } } ftpClient.on('ready', () => { if (filename) { let totalsize = 0; ftpClient.cwd(dir, (err) => { if (err) { this.sendToClient(socket, { 'Status': '500 Failed to change directory', 'Content-Type': 'text/plain' }, 'Failed to change directory'); ftpClient.end(); return; } ftpClient.get(filename, (err, stream) => { if (err) { this.sendToClient(socket, { 'Status': '404 File not found', 'Content-Type': 'text/plain' }, 'File not found'); ftpClient.end(); return; } const chunks = []; stream.on('data', (chunk) => { chunks.push(chunk); totalsize += chunk.length; if (totalsize > 1024 * 1024 * this.minisrv_config.services[this.service_name].max_response_size) { this.sendToClient(socket, { 'Status': '413 The item chosen contains too much information to be used.', 'Content-Type': 'text/plain' }, 'Item too large'); ftpClient.end(); return; } }); stream.on('end', () => { const buffer = Buffer.concat(chunks); const mime = this.wtvmime.detectMimeTypeFromBuffer(buffer); this.sendToClient( socket, { 'Status': 200, 'Content-Type': mime || 'application/octet-stream', 'Content-Disposition': `attachment; filename="${filename}"` }, buffer ); ftpClient.end(); }); stream.on('error', () => { this.sendToClient(socket, { 'Status': '500 Error reading file', 'Content-Type': 'text/plain' }, 'Error reading file'); ftpClient.end(); }); }); }); } else { ftpClient.list(dir, (err, list) => { if (err) { this.sendToClient(socket, { 'Status': '500 Failed to list directory', 'Content-Type': 'text/plain' }, 'Failed to list directory'); ftpClient.end(); return; } const html = this.formatDirectoryListing(list); this.sendToClient(socket, { 'Status': '200 OK', 'Content-Type': 'text/html' }, html); ftpClient.end(); }); } }); ftpClient.on('error', (err) => { this.sendToClient(socket, { 'Status': '500 FTP connection error', 'Content-Type': 'text/plain' }, 'FTP connection error'); }); // FIX: Resolve host to IPv4 address before connecting this.dns.lookup(host, { family: 4 }, (err, address) => { if (err) { this.sendToClient(socket, { 'Status': '500 DNS resolution error', 'Content-Type': 'text/plain' }, 'DNS resolution error'); return; } ftpClient.connect({ host: address, port: port, user: user, password: pass }); }); } formatDirectoryListing(list) { const html = ` FTP Directory Listing

FTP Directory Listing

${list.map(item => { const dateStr = item.date ? item.date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : ''; return ` `; }).join('')}
Type Size Date
${item.type === 'd' ? '' : ''} ${item.name} ${item.size !== undefined ? this.wtvshared.formatBytes(item.size) : ''} ${dateStr}
`; return html; } } module.exports = WTVFTP;