From 385849dab78adffaec6c996b7206d3c4ef5f3efb Mon Sep 17 00:00:00 2001 From: zefie Date: Sun, 20 Jul 2025 18:32:06 -0400 Subject: [PATCH] move functions to wtvshared, initial wtvproxy filtering code by flamelord --- zefie_wtvp_minisrv/app.js | 83 ++---------------- .../includes/classes/WTVProxy.js | 86 +++++++++++++++++++ .../includes/classes/WTVShared.js | 85 ++++++++++++++++++ 3 files changed, 176 insertions(+), 78 deletions(-) create mode 100644 zefie_wtvp_minisrv/includes/classes/WTVProxy.js diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index d9f35e2b..b9d40e4d 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -464,7 +464,7 @@ async function handleCGI(executable, cgi_file, socket, request_headers, vault, s var stdout = data.split("\r\n\r\n", 2); var headers = stdout[0]; data = stdout[1]; - headers = headerStringToObj(headers, true); + headers = wtvshared.headerStringToObj(headers, true); if (!headers.Status) headers.Status = "200 OK"; headers['Connection'] = 'keep-alive'; sendToClient(socket, headers, data); @@ -1167,7 +1167,7 @@ function handleProxy(socket, request_type, request_headers, res, data) { // header pass-through whitelist, case insensitive comparsion to server, however, you should // specify the header case as you intend for the client - var headers = stripHeaders(res.headers, [ + var headers = wtvshared.stripHeaders(res.headers, [ 'Connection', 'Server', 'Date', @@ -1345,79 +1345,6 @@ async function doHTTPProxy(socket, request_headers) { } } -function stripHeaders(headers_obj, whitelist) { - var whitelisted_headers = new Array(); - var out_headers = new Array(); - out_headers.Status = headers_obj.Status; - if (headers_obj['wtv-connection-close']) out_headers['wtv-connection-close'] = headers_obj['wtv-connection-close']; - - // compare regardless of case - Object.keys(whitelist).forEach(function (k) { - Object.keys(headers_obj).forEach(function (j) { - if (whitelist[k].toLowerCase() == j.toLowerCase()) { - // if header = connection, strip 'upgrade' - if (j.toLowerCase() == "connection") { - headers_obj[j] = headers_obj[j].replace("Upgrade", "").replace(",", "").trim(); - } - whitelisted_headers[j.toLowerCase()] = [whitelist[k], j, headers_obj[j]]; - } - }); - }); - - // restore original header order - Object.keys(headers_obj).forEach(function (k) { - if (whitelisted_headers[k.toLowerCase()]) { - if (whitelisted_headers[k.toLowerCase()][1] == k) out_headers[whitelisted_headers[k.toLowerCase()][0]] = whitelisted_headers[k.toLowerCase()][2]; - } - }); - - // return - return out_headers; -} - -function headerStringToObj(headers, response = false) { - var inc_headers = 0; - var headers_obj = {}; - headers_obj.raw_headers = headers; - var headers_obj_pre = headers.split("\n"); - headers_obj_pre.forEach(function (d) { - if (/^SECURE ON/.test(d) && !response) { - headers_obj.secure = true; - } else if (/^([0-9]{3}) $/.test(d.substring(0, 4)) && response && !headers_obj.Status) { - d.s - headers_obj.Status = d.trim("\r"); - } else if (/^(GET |PUT |POST)$/.test(d.substring(0, 4)) && !response) { - headers_obj.request = d.trim("\r"); - var request_url = d.split(' '); - if (request_url.length > 2) { - request_url.shift(); - request_url = request_url.join(" "); - if (request_url.indexOf("HTTP/") > 0) { - var index = request_url.indexOf(" HTTP/"); - request_url = request_url.substring(0, index); - } - } else { - request_url = request_url[1]; - } - headers_obj.request_url = decodeURI(request_url).trim("\r"); - } else if (d.indexOf(":") > 0) { - var d_split = d.split(':'); - var header_name = d_split[0]; - if (headers_obj[header_name] != null) { - header_name = header_name + "_" + inc_headers; - inc_headers++; - } - d_split.shift(); - d = d_split.join(':'); - headers_obj[header_name] = (d).trim("\r"); - if (headers_obj[header_name].substring(0, 1) == " ") { - headers_obj[header_name] = headers_obj[header_name].substring(1); - } - } - }); - return headers_obj; -} - async function sendToClient(socket, headers_obj, data = null) { var headers = ""; var content_length = 0; @@ -1427,7 +1354,7 @@ async function sendToClient(socket, headers_obj, data = null) { if (typeof (data) === 'undefined' || data === null) data = ''; if (typeof (headers_obj) === 'string') { // string to header object - headers_obj = headerStringToObj(headers_obj, true); + headers_obj = wtvshared.headerStringToObj(headers_obj, true); } if (!socket_sessions[socket.id]) { if (socket.destroy) socket.destroy(); @@ -1800,13 +1727,13 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } if (isUnencryptedString(data)) { if (headers.length != 0) { - var new_header_obj = headerStringToObj(data); + var new_header_obj = wtvshared.headerStringToObj(data); Object.keys(new_header_obj).forEach(function (k, v) { headers[k] = new_header_obj[k]; }); new_header_obj = null; } else { - headers = headerStringToObj(data); + headers = wtvshared.headerStringToObj(data); } } else if (!skipSecure) { // if its a POST request, assume its a binary blob and not encrypted (dangerous) diff --git a/zefie_wtvp_minisrv/includes/classes/WTVProxy.js b/zefie_wtvp_minisrv/includes/classes/WTVProxy.js new file mode 100644 index 00000000..f78fa81d --- /dev/null +++ b/zefie_wtvp_minisrv/includes/classes/WTVProxy.js @@ -0,0 +1,86 @@ +'use strict'; +const { WTVShared, clientShowAlert } = require("./WTVShared.js"); + +class WTVProxy { + constructor(minisrv_config) { + this.minisrv_config = minisrv_config; + this.wtvshared = new WTVShared(this.minisrv_config); + } + + transformHtml(html) { + try { + // Apply existing transformations + let transformed = html + .replace(/[^\x20-\x7E\n\r\t]/g, '') // Remove non-ASCII + .replace(/\s+/g, ' ') // Collapse whitespace + .replace(//g, '') // Remove comments + .replace(/)<[^<]*)*<\/script>/gi, '') // Remove scripts + .replace(/)<[^<]*)*>/gi, '') // Remove meta tags + .replace(/)<[^<]*)*>/gi, '') // Remove images + .replace(/)<[^<]*)*>/gi, '') // Remove input tags + .replace(/)<[^<]*)*>/gi, '') // Remove link tags + .replace(/)<[^<]*)*>/gi, '') // Remove embed tags + .replace(/)<[^<]*)*>/gi, '') // Remove links + .replace(/<\/a>/gi, '') // Remove closing links + .replace(/)<[^<]*)*<\/iframe>/gi, '') + .replace(/)<[^<]*)*<\/object>/gi, '') + .replace(/javascript:/gi, '') + .replace(/on\w+\s*=\s*("[^"]*"|'[^']*'|[^ >]+)/gi, '') + .replace(/style\s*=\s*("[^"]*"|'[^']*'|[^ >]+)/gi, '') + .replace(/class\s*=\s*("[^"]*"|'[^']*'|[^ >]+)/gi, '') + .replace(/id\s*=\s*("[^"]*"|'[^']*'|[^ >]+)/gi, '') + .replace(/<(div|span|section|article|aside|header|footer|nav)\b/gi, '') + .replace(/<\/(div|span|section|article|aside|header|footer|nav)>/gi, '') + .replace(/FP_preloadImgs\s*\(.*?\)/gi, ''); + + // Normalize for processing + transformed = transformed + .replace(/>\s+<') // Remove accidental whitespace between tags + .replace(//g, '>\n') // Add newline after each tag + .replace(/\n\s*\n/g, '\n'); // Collapse multiple newlines + + // Format with indentation + const lines = transformed.split('\n'); + let indentLevel = 0; + const indentSize = 2; + + const formatted = lines.map((line) => { + const trimmed = line.trim(); + if (trimmed === '') return ''; + + const isClosing = /^<\/.+?>/.test(trimmed); + const isSelfClosing = /^<.+?\/>$/.test(trimmed) || + /^
).*?>/.test(trimmed) && !isClosing; + + if (isClosing) indentLevel = Math.max(indentLevel - 1, 0); + + const indentedLine = ' '.repeat(indentLevel * indentSize) + trimmed; + + if (isOpening && !isSelfClosing) indentLevel++; + + return indentedLine; + }); + + transformed = formatted.join('\n').trim(); + + // Wrap in DOCTYPE and HTML structure + transformed = `\n\n \n \n \n \n${transformed}\n \n`; + + // Truncate if necessary + if (transformed.length > 512) { + transformed = transformed.substring(0, 512); + transformed = transformed.substring(0, transformed.lastIndexOf('<')) + '\n \n'; + } + + return Buffer.from(transformed, 'ascii').toString('ascii'); + } catch (err) { + throw new Error(`HTML transformation failed: ${err.message}`); + } + } +} + +module.exports = WTVProxy; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/classes/WTVShared.js b/zefie_wtvp_minisrv/includes/classes/WTVShared.js index 103133bb..1040afb9 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVShared.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVShared.js @@ -327,6 +327,91 @@ class WTVShared { return clean; } + /** + * Converts a header string into an object + * @param {string} headers Header string to convert + * @param {boolean} response If true, the headers are a response, otherwise they are a request + * @return {object} Headers object + * */ + headerStringToObj(headers, response = false) { + var inc_headers = 0; + var headers_obj = {}; + headers_obj.raw_headers = headers; + var headers_obj_pre = headers.split("\n"); + headers_obj_pre.forEach(function (d) { + if (/^SECURE ON/.test(d) && !response) { + headers_obj.secure = true; + } else if (/^([0-9]{3}) $/.test(d.substring(0, 4)) && response && !headers_obj.Status) { + d.s + headers_obj.Status = d.trim("\r"); + } else if (/^(GET |PUT |POST)$/.test(d.substring(0, 4)) && !response) { + headers_obj.request = d.trim("\r"); + var request_url = d.split(' '); + if (request_url.length > 2) { + request_url.shift(); + request_url = request_url.join(" "); + if (request_url.indexOf("HTTP/") > 0) { + var index = request_url.indexOf(" HTTP/"); + request_url = request_url.substring(0, index); + } + } else { + request_url = request_url[1]; + } + headers_obj.request_url = decodeURI(request_url).trim("\r"); + } else if (d.indexOf(":") > 0) { + var d_split = d.split(':'); + var header_name = d_split[0]; + if (headers_obj[header_name] != null) { + header_name = header_name + "_" + inc_headers; + inc_headers++; + } + d_split.shift(); + d = d_split.join(':'); + headers_obj[header_name] = (d).trim("\r"); + if (headers_obj[header_name].substring(0, 1) == " ") { + headers_obj[header_name] = headers_obj[header_name].substring(1); + } + } + }); + return headers_obj; + } + + /** + * Strips headers not in the whitelist + * @param {object} headers_obj // Headers object to strip + * @param {Array} whitelist // Array of header names to keep, case insensitive + * @returns {object} // Headers object with only whitelisted headers + */ + stripHeaders(headers_obj, whitelist) { + var whitelisted_headers = new Array(); + var out_headers = new Array(); + out_headers.Status = headers_obj.Status; + if (headers_obj['wtv-connection-close']) out_headers['wtv-connection-close'] = headers_obj['wtv-connection-close']; + + // compare regardless of case + Object.keys(whitelist).forEach(function (k) { + Object.keys(headers_obj).forEach(function (j) { + if (whitelist[k].toLowerCase() == j.toLowerCase()) { + // if header = connection, strip 'upgrade' + if (j.toLowerCase() == "connection") { + headers_obj[j] = headers_obj[j].replace("Upgrade", "").replace(",", "").trim(); + } + whitelisted_headers[j.toLowerCase()] = [whitelist[k], j, headers_obj[j]]; + } + }); + }); + + // restore original header order + Object.keys(headers_obj).forEach(function (k) { + if (whitelisted_headers[k.toLowerCase()]) { + if (whitelisted_headers[k.toLowerCase()][1] == k) out_headers[whitelisted_headers[k.toLowerCase()][0]] = whitelisted_headers[k.toLowerCase()][2]; + } + }); + + // return + return out_headers; + } + /** * Attempts to determine if the string is ASCII * @param {string} str