update modules, add ftp:// support
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 744 B |
187
zefie_wtvp_minisrv/includes/classes/WTVFTP.js
Normal file
187
zefie_wtvp_minisrv/includes/classes/WTVFTP.js
Normal file
@@ -0,0 +1,187 @@
|
||||
class WTVFTP {
|
||||
wtvshared = null;
|
||||
wtvmime = null;
|
||||
minisrv_config = null;
|
||||
sendToClient = null;
|
||||
request_headers = null;
|
||||
ftp = null;
|
||||
url = null;
|
||||
|
||||
constructor(minisrv_config, sendToClient) {
|
||||
this.minisrv_config = minisrv_config;
|
||||
this.sendToClient = sendToClient;
|
||||
const WTVShared = require("./WTVShared.js")['WTVShared'];
|
||||
const WTVMime = require("./WTVMime.js");
|
||||
this.url = require('url');
|
||||
this.ftp = require('ftp');
|
||||
this.wtvshared = new WTVShared();
|
||||
this.wtvmime = new WTVMime();
|
||||
}
|
||||
|
||||
handleFTPRequest(socket, request_headers) {
|
||||
// Handle the FTP request here
|
||||
// Assume request_headers.url contains the FTP URL
|
||||
this.request_headers = request_headers;
|
||||
const ftpUrl = request_headers.request_url;
|
||||
const parsed = this.url.parse(ftpUrl);
|
||||
|
||||
// Extract user, pass, and host
|
||||
let user = null;
|
||||
let pass = null;
|
||||
let host = parsed.hostname;
|
||||
|
||||
if (parsed.auth) {
|
||||
const [username, password] = parsed.auth.split(':');
|
||||
user = username;
|
||||
pass = password || null;
|
||||
}
|
||||
|
||||
// Example usage: log the parsed values
|
||||
|
||||
|
||||
// You can now use user, pass, and host as needed
|
||||
if (!user && !pass) {
|
||||
user = "anonymous";
|
||||
pass = "anonymous@eff.org";
|
||||
}
|
||||
console.log(`User: ${user}, Pass: ${pass}, Host: ${host}`);
|
||||
|
||||
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;
|
||||
|
||||
// Determine if path is a file or directory
|
||||
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) {
|
||||
var totalsize = 0;
|
||||
// Change to directory and get file
|
||||
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 * 4) {
|
||||
this.sendToClient(socket, { 'Status': '413 The file chosen contains too much information to be used.', 'Content-Type': 'text/plain' }, 'File 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 {
|
||||
// List directory
|
||||
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');
|
||||
});
|
||||
|
||||
ftpClient.connect({
|
||||
host: host,
|
||||
port: port,
|
||||
user: user,
|
||||
password: pass
|
||||
});
|
||||
}
|
||||
|
||||
formatDirectoryListing(list) {
|
||||
// Format the directory listing as needed
|
||||
let 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;
|
||||
@@ -200,6 +200,98 @@ class WTVMime {
|
||||
return new Array(wtv_mime_type, modern_mime_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to detect the MIME type from a data buffer using magic numbers.
|
||||
* Falls back to 'application/octet-stream' if unknown.
|
||||
* @param {Buffer} buffer
|
||||
* @returns {string} Detected MIME type
|
||||
*/
|
||||
detectMimeTypeFromBuffer(buffer) {
|
||||
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
// JPEG
|
||||
if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
// PNG
|
||||
if (buffer.slice(0, 8).equals(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))) {
|
||||
return 'image/png';
|
||||
}
|
||||
// GIF
|
||||
if (buffer.slice(0, 6).toString() === 'GIF87a' || buffer.slice(0, 6).toString() === 'GIF89a') {
|
||||
return 'image/gif';
|
||||
}
|
||||
// PDF
|
||||
if (buffer.slice(0, 4).toString() === '%PDF') {
|
||||
return 'application/pdf';
|
||||
}
|
||||
// ZIP
|
||||
if (buffer[0] === 0x50 && buffer[1] === 0x4B && (buffer[2] === 0x03 || buffer[2] === 0x05 || buffer[2] === 0x07) && (buffer[3] === 0x04 || buffer[3] === 0x06 || buffer[3] === 0x08)) {
|
||||
return 'application/zip';
|
||||
}
|
||||
// GZIP
|
||||
if (buffer[0] === 0x1F && buffer[1] === 0x8B) {
|
||||
return 'application/gzip';
|
||||
}
|
||||
// MP3
|
||||
if ((buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33) || (buffer[0] === 0xFF && (buffer[1] & 0xE0) === 0xE0)) {
|
||||
return 'audio/mpeg';
|
||||
}
|
||||
// WAV
|
||||
if (buffer.slice(0, 4).toString() === 'RIFF' && buffer.slice(8, 12).toString() === 'WAVE') {
|
||||
return 'audio/wav';
|
||||
}
|
||||
// WebP
|
||||
if (buffer.slice(0, 4).toString() === 'RIFF' && buffer.slice(8, 12).toString() === 'WEBP') {
|
||||
return 'image/webp';
|
||||
}
|
||||
// BMP
|
||||
if (buffer[0] === 0x42 && buffer[1] === 0x4D) {
|
||||
return 'image/bmp';
|
||||
}
|
||||
// OGG
|
||||
if (buffer.slice(0, 4).toString() === 'OggS') {
|
||||
return 'application/ogg';
|
||||
}
|
||||
// MIDI
|
||||
if (buffer.slice(0, 4).toString() === 'MThd') {
|
||||
return 'audio/midi';
|
||||
}
|
||||
// TAR
|
||||
if (buffer.length > 257 && buffer.slice(257, 262).toString() === 'ustar') {
|
||||
return 'application/x-tar';
|
||||
}
|
||||
// TEXT (plain)
|
||||
if (
|
||||
buffer.length >= 4 &&
|
||||
(
|
||||
buffer.slice(0, 5).toString().toLowerCase() === '<?xml' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<html>' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<!doc' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<head>' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<body>' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<meta>' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<titl>' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<scri>' ||
|
||||
buffer.slice(0, 6).toString().toLowerCase() === '<styl>'
|
||||
)
|
||||
) {
|
||||
return 'text/html';
|
||||
}
|
||||
// Try to detect plain text (no null bytes, mostly printable)
|
||||
if (
|
||||
buffer.length > 0 &&
|
||||
buffer.slice(0, 512).every(b => (b === 0x09 || b === 0x0A || b === 0x0D || (b >= 0x20 && b <= 0x7E)))
|
||||
) {
|
||||
return 'text/plain';
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
// modified from https://github.com/sergi/mime-multipart/blob/master/index.js
|
||||
|
||||
generateMultipartMIME(tuples, options) {
|
||||
|
||||
@@ -979,6 +979,23 @@ class WTVShared {
|
||||
return encoded.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a bytes value into a human-readable string (KB, MB, GB)
|
||||
* @param {number} bytes The number of bytes
|
||||
* @param {number} decimals The number of decimal places to include in the output (default is 2)
|
||||
* @returns {string} Human-readable string with 2 decimal places
|
||||
*/
|
||||
formatBytes(bytes, decimals = 2) {
|
||||
if (typeof bytes !== 'number' || isNaN(bytes)) return '0 Bytes';
|
||||
const units = ['B', 'KB', 'MB'];
|
||||
let i = 0;
|
||||
while (bytes >= 1024 && i < units.length - 1) {
|
||||
bytes /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${bytes.toFixed(decimals)} ${units[i]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a urlencoded string into a binary buffer
|
||||
* @param {string} encoded urlencoded string
|
||||
|
||||
@@ -346,6 +346,10 @@
|
||||
"WTVAuthor"
|
||||
]
|
||||
},
|
||||
"ftp": {
|
||||
"port": 1650,
|
||||
"connections": 3
|
||||
},
|
||||
"http": {
|
||||
// http upstream
|
||||
"port": 1650,
|
||||
|
||||
Reference in New Issue
Block a user