183 lines
7.0 KiB
JavaScript
183 lines
7.0 KiB
JavaScript
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 = `<html>
|
|
<head>
|
|
<title>FTP Directory Listing</title>
|
|
<style>
|
|
table { border-collapse: collapse; width: 100%; }
|
|
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
|
|
th { background: #f4f4f4; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>FTP Directory Listing</h2>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th> </th>
|
|
<th>Type</th>
|
|
<th>Size</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${list.map(item => {
|
|
const dateStr = item.date
|
|
? item.date.toLocaleString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
: '';
|
|
return `
|
|
<tr>
|
|
<td>${item.type === 'd' ? '<img src="wtv-star:/ROMCache/DirectoryIcon.png" width=16 height=16>' : '<img src="wtv-star:/ROMCache/FileIcon.png" width=16 height=16>'}</td>
|
|
<td><a href="${this.request_headers.request_url}${item.name}${item.type === 'd' ? '/' : ''}">${item.name}</a></td>
|
|
<td>${item.size !== undefined ? this.wtvshared.formatBytes(item.size) : ''}</td>
|
|
<td>${dateStr}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>`;
|
|
return html;
|
|
}
|
|
}
|
|
|
|
module.exports = WTVFTP;
|