diff --git a/zefie_wtvp_minisrv/includes/classes/WTVShared.js b/zefie_wtvp_minisrv/includes/classes/WTVShared.js index 07295b62..8f69d08e 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVShared.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVShared.js @@ -8,6 +8,7 @@ class WTVShared { path = require('path'); fs = require('fs'); + readline = require('readline'); v8 = require('v8'); zlib = require('zlib'); html_entities = require('html-entities'); // used externally by service scripts @@ -139,84 +140,52 @@ class WTVShared { } parseJSON(json) { - if (!json) return null; - if (typeof json !== 'string') json = json.toString(); + if (typeof json !== 'string') json = json ? json.toString() : ''; + let result = ''; + let i = 0; + let isString = false; + let isEscape = false; + let isBlockComment = false; + let isLineComment = false; - // from https://github.com/getify/JSON.minify/blob/javascript/minify.json.js - var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g, - in_string = false, - in_multiline_comment = false, - in_singleline_comment = false, - tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc, - prevFrom - ; + while (i < json.length) { + const char = json[i]; + const nextChar = json[i + 1]; - tokenizer.lastIndex = 0; - - while (tmp = tokenizer.exec(json)) { - lc = RegExp.leftContext; - rc = RegExp.rightContext; - if (!in_multiline_comment && !in_singleline_comment) { - tmp2 = lc.substring(from); - if (!in_string) { - tmp2 = tmp2.replace(/(\n|\r|\s)+/g, ""); + if (!isString && !isEscape && char === '/' && nextChar === '*') { + isBlockComment = true; + i++; + } else if (isBlockComment && char === '*' && nextChar === '/') { + isBlockComment = false; + i++; + } else if (!isString && !isEscape && char === '/' && nextChar === '/') { + isLineComment = true; + i++; + } else if (isLineComment && (char === '\n' || char === '\r')) { + isLineComment = false; + } else if (!isBlockComment && !isLineComment) { + if (char === '"' && !isEscape) { + isString = !isString; } - new_str[ns++] = tmp2; - } - prevFrom = from; - from = tokenizer.lastIndex; - - // found a " character, and we're not currently in - // a comment? check for previous `\` escaping immediately - // leftward adjacent to this match - if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) { - // perform look-behind escaping match, but - // limit left-context matching to only go back - // to the position of the last token match - // - // see: https://github.com/getify/JSON.minify/issues/64 - tmp2 = lc.substring(prevFrom).match(/\\+$/); - - // start of string with ", or unescaped " character found to end string? - if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { - in_string = !in_string; - } - from--; // include " character in next catch - rc = json.substring(from); - } - else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) { - in_multiline_comment = true; - } - else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) { - in_multiline_comment = false; - } - else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) { - in_singleline_comment = true; - } - else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) { - in_singleline_comment = false; - } - else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) { - new_str[ns++] = tmp[0]; + isEscape = char === '\\' && !isEscape; + result += char; } + i++; } - new_str[ns++] = rc; - return JSON.parse(new_str.join("")); + + return JSON.parse(result); } + /** * Attempts to convert val into a boolean * @param {string,int,boolean} val * @returns {boolean} */ parseBool(val) { - if (typeof val === 'string') - val = val.toLowerCase(); - - return (val === true || val == "on" || val === "true" || val === 1); + return !!(val && /^(true|1|on|yes)$/i.test(val.toString().trim())); } - getQueryString(query) { // for easy retrofitting old code to work with the webtvism of allowing multiple of the same query name // pass it the query, and it will return a string regardless. if its a string it just sends it back @@ -234,17 +203,32 @@ class WTVShared { * @returns {string} Entitized string */ htmlEntitize(string, process_newline = false) { - if (this.shenanigans.checkShenanigan(this.shenanigans.shenanigans.DISABLE_HTML_ENTITIZER)) { - // shenanigans level matches, don't encode + // Assuming checkShenanigan returns a boolean + if (this.shenanigans && this.shenanigans.checkShenanigan(this.shenanigans.shenanigans.DISABLE_HTML_ENTITIZER)) { return string; } - string = this.html_entities.encode(string).replace(/'/g, "'"); + // Directly replace &, <, >, ", and ' characters + let entitized = string.replace(/[&<>"]/g, function (match) { + switch (match) { + case '&': return '&'; + case '<': return '<'; + case '>': return '>'; + case '"': return '"'; + case "'": return '''; + // ' is not needed to be replaced since it is valid in HTML5 and XHTML + default: return match; + } + }); - if (process_newline) string = string.replace(/\n/gi, "
").replace(/\r/gi, ""); - return string; + if (process_newline) { + entitized = entitized.replace(/\n/g, "
").replace(/\r/g, ""); + } + + return entitized; } + /** * Attempts to sanitize HTML code to remove possible exploits when embedded in a WebTV Service * @param {string} string The string to sanitize @@ -319,77 +303,54 @@ class WTVShared { * @returns {boolean} true if ASCII only, otherwise false */ isASCII(str) { - if (typeof str !== 'string') return false; - for (var i = 0, strLen = str.length; i < strLen; ++i) { - if (str.charCodeAt(i) > 127) return false; - } - return true; + return typeof str === 'string' && /^[\x00-\x7F]*$/.test(str); } + /** * Attempts to determine if the string contains HTML * @param {string} str * @returns {boolean} true if HTML detected, otherwise false */ isHTML(str) { - return /<\/?[a-z][\s\S]*>/i.test() + const pattern = /<([A-Za-z][A-Za-z0-9]*)\b[^>]*>(.*?)<\/\1>/; + return typeof str === 'string' && pattern.test(str); } + /** * Attempts to determine if the string is Base64 or not * @param {string} str String to check * @param {object} opts * @return {boolean} true if Base64, otherwise false */ - isBase64(str, opts) { - // from https://github.com/miguelmota/is-base64/blob/master/is-base64.js - if (str instanceof Boolean || typeof str === 'boolean') { - return false + isBase64(str, opts = {}) { + if (typeof str !== 'string' || (opts.allowEmpty === false && str === '')) { + return false; } - if (!(opts instanceof Object)) { - opts = {} - } + // Create a regex string based on the provided options + const regexBase = '[A-Za-z0-9+\\/]'; + const regexPadding = opts.paddingRequired === false ? '=?' : '='; + const regexMime = opts.mimeRequired ? 'data:\\w+\\/[-+.\\w]+;base64,' : ''; - if (opts.allowEmpty === false && str === '') { - return false - } + // Construct the final regex with the appropriate groups + const regex = `^${regexMime}(?:${regexBase}{4})*${opts.allowMime ? '?' : ''}(?:${regexBase}{2}${regexPadding}{2}|${regexBase}{3}${regexPadding})?$`; - var regex = '(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\/]{3}=)?' - var mimeRegex = '(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)' - - if (opts.mimeRequired === true) { - regex = mimeRegex + regex - } else if (opts.allowMime === true) { - regex = mimeRegex + '?' + regex - } - - if (opts.paddingRequired === false) { - regex = '(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?' - } - - return (new RegExp('^' + regex + '$', 'gi')).test(str) + return new RegExp(regex, 'gi').test(str); } + utf8Decode(utf8String) { - if (typeof utf8String != 'string') throw new TypeError('parameter ‘utf8String’ is not a string'); - // note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char! - const unicodeString = utf8String.replace( - /[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars - function (c) { // (note parentheses for precedence) - var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f); - return String.fromCharCode(cc); - } - ).replace( - /[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars - function (c) { // (note parentheses for precedence) - var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f; - return String.fromCharCode(cc); - } - ); - return unicodeString; + if (typeof utf8String !== 'string') { + throw new TypeError('parameter ‘utf8String’ is not a string'); + } + const textDecoder = new TextDecoder('utf-8'); + const bytes = new Uint8Array(utf8String.split('').map(c => c.charCodeAt(0))); + return textDecoder.decode(bytes); } + decodeBufferText(buf) { var out = ""; out = this.utf8Decode(this.iconv.decode(Buffer.from(buf),'ISO-8859-1')); @@ -402,15 +363,16 @@ class WTVShared { * @return {string} The absolute path */ returnAbsolutePath(check_path) { - if (check_path.substring(0, 1) != this.path.sep && check_path.substring(1, 2) != ":") { - // non-absolute path, so use current directory as base + // Assuming this.path.sep is a slash (/ or \) and this.parentDirectory is set correctly + if (!/^(?:[a-zA-Z]:)?[\\/]/.test(check_path)) { + // It's a relative path check_path = this.parentDirectory + this.path.sep + check_path; - } else { - // already absolute path } + // Use the fixPathSlashes method to normalize the slashes return this.fixPathSlashes(check_path); } + /** * Detects if the client is in MiniBrowser mode * @param {object} ssid_session @@ -462,58 +424,45 @@ class WTVShared { * @returns {object} ssid info object */ parseSSID(ssid) { - var ssid_obj = {}; - switch (ssid.substring(0, 2)) { - case "01": - ssid_obj.boxType = "Internal"; - break; - case "81": - ssid_obj.boxType = "Retail"; - break; - case "91": - // not a definitive way to detect a viewer - ssid_obj.boxType = "Viewer"; - break; - } - ssid_obj.unique_id = ssid.substring(2, 8); - switch (ssid.substring(10, 14).toUpperCase()) { - case "B002": - ssid_obj.region = "US/Canada"; - break; - case "B102": - ssid_obj.region = "Japan"; - break; + const boxTypeMapping = { + "01": "Internal", + "81": "Retail", + "91": "Viewer" + }; + + const regionMapping = { + "B002": "US/Canada", + "B102": "Japan" + }; + + const manufacturerMapping = { + "00": "Sony", // Default to Sony if the region is not Japan + "10": "Philips", + "50": "Philips", + "40": "Mitsubishi", + "70": "Samsung", + "80": "EchoStar", + "90": "RCA", + "AE": "zefie & MattMan69" + }; + + const ssid_obj = { + boxType: boxTypeMapping[ssid.substring(0, 2)], + unique_id: ssid.substring(2, 8), + region: regionMapping[ssid.substring(10, 14).toUpperCase()], + manufacturer: manufacturerMapping[ssid.substring(8, 10).toUpperCase()], + crc: ssid.substring(14) + }; + + // Special case for manufacturer based on region + if (ssid_obj.region === "Japan" && ssid.substring(8, 10).toUpperCase() === "00") { + ssid_obj.manufacturer = "Panasonic"; } - switch (ssid.substring(8, 10).toUpperCase()) { - case "00": - if (ssid_obj.region == "Japan") ssid_obj.manufacturer = "Panasonic"; - else ssid_obj.manufacturer = "Sony"; - break; - case "10": - case "50": - ssid_obj.manufacturer = "Philips"; - break; - case "40": - ssid_obj.manufacturer = "Mitsubishi"; - break; - case "70": - ssid_obj.manufacturer = "Samsung"; - break; - case "80": - ssid_obj.manufacturer = "EchoStar"; - break; - case "90": - ssid_obj.manufacturer = "RCA"; - break; - case "AE": - ssid_obj.manufacturer = "zefie & MattMan69"; - break; - } - ssid_obj.crc = ssid.substring(14) return ssid_obj; } + /** * Alias for parseSSID, but just the manufacture info * @param {string} ssid @@ -534,123 +483,114 @@ class WTVShared { * @returns {object} The modified object */ moveObjectElement(currentKey, afterKey, obj, caseInsensitive = false) { - var result = {}; + let keys = Object.keys(obj); + let values = Object.values(obj); + if (caseInsensitive) { - Object.keys(obj).forEach((k) => { - if (k.toLowerCase() == currentKey.toLowerCase()) { - currentKey = k; - return false; - } - }) - } - var val = obj[currentKey]; - delete obj[currentKey]; - var next = -1; - var i = 0; - if (typeof afterKey == 'undefined' || afterKey == null) afterKey = ''; - Object.keys(obj).forEach(function (k) { - var v = obj[k]; - if ((afterKey == '' && i == 0) || next == 1) { - result[currentKey] = val; - next = 0; - } - if (k == afterKey || (caseInsensitive && k.toLowerCase() == afterKey.toLowerCase())) { next = 1; } - result[k] = v; - ++i; - }); - if (next == 1) { - result[currentKey] = val; + const lowerCurrentKey = currentKey.toLowerCase(); + const foundKey = keys.find(k => k.toLowerCase() === lowerCurrentKey); + currentKey = foundKey || currentKey; } - if (next !== -1) return result; else return obj; + + const afterKeyIndex = typeof afterKey === 'string' ? + keys.findIndex(k => caseInsensitive ? k.toLowerCase() === afterKey.toLowerCase() : k === afterKey) + : -1; + + if (afterKeyIndex === -1) { + // If afterKey is not found or not defined, don't move the element + return obj; + } + + const currentIndex = keys.indexOf(currentKey); + if (currentIndex === -1) { + // If currentKey is not found, don't move the element + return obj; + } + + // Remove the current element from keys and values + const [currentKeyValue] = values.splice(currentIndex, 1); + keys.splice(currentIndex, 1); + + // Insert the current element after the afterKey position + keys.splice(afterKeyIndex + 1, 0, currentKey); + values.splice(afterKeyIndex + 1, 0, currentKeyValue); + + // Reconstruct the object with the new order + const result = {}; + keys.forEach((key, index) => { + result[key] = values[index]; + }); + + return result; } + readMiniSrvConfig(user_config = true, notices = true, reload_notice = false) { - if (notices || reload_notice) console.log(" *** Reading global configuration..."); + const log = (msg) => { + if (notices || reload_notice) console.log(msg); + }; + const logError = (msg, e) => { + console.error(msg, e); + if (this.minisrv_config && this.minisrv_config.config.debug_flags && this.minisrv_config.config.debug_flags.debug) { + console.error(" * Notice: Using default configuration."); + } + }; + + log(" *** Reading global configuration..."); try { var minisrv_config = this.parseJSON(this.fs.readFileSync(this.getAbsolutePath(".." + this.path.sep + "config.json", __dirname))); } catch (e) { - throw ("ERROR: Could not read config.json", e); - } - - var integrateConfig = function(main, user) { - Object.keys(user).forEach(function (k) { - if (typeof (user[k]) == 'object' && user[k] != null) { - // new entry - if (!main[k]) main[k] = new Array(); - // go down the rabbit hole - main[k] = integrateConfig(main[k], user[k]); - } else { - // update main config - main[k] = user[k]; - } - }); - return main; + throw new Error("ERROR: Could not read config.json", e); } if (user_config) { + log(" *** Reading user configuration..."); try { - if (notices || reload_notice) console.log(" *** Reading user configuration..."); - var minisrv_user_config = this.getUserConfig() - if (!minisrv_user_config) throw "WARN: Could not read user_config.json"; - try { - minisrv_config = integrateConfig(minisrv_config, minisrv_user_config) - } catch (e) { - console.error("ERROR: Could not read user_config.json", e); - this.process.exit(1); - } + let minisrv_user_config = this.getUserConfig(); + if (!minisrv_user_config) throw new Error("WARN: Could not read user_config.json"); + minisrv_config = this.integrateConfig(minisrv_config, minisrv_user_config); } catch (e) { - if (minisrv_config.config.debug_flags) { - if (minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration."); - } + logError("ERROR: Could not integrate user_config.json", e); + this.process.exit(1); } } - // defaults - minisrv_config.config.debug_flags = []; - minisrv_config.config.debug_flags.debug = false; - minisrv_config.config.debug_flags.quiet = true; // will squash minisrv_config.config.debug_flags.debug even if its true - minisrv_config.config.debug_flags.show_headers = false; + // Set debug flags based on verbosity + const debugFlags = { + debug: false, + quiet: true, // will squash debug even if its true + show_headers: false + }; - if (minisrv_config.config.verbosity) { - switch (minisrv_config.config.verbosity) { - case 0: - minisrv_config.config.debug_flags.debug = false; - minisrv_config.config.debug_flags.quiet = true; - minisrv_config.config.debug_flags.show_headers = false; - if (notices) console.log(" * Console Verbosity level 0 (quietest)") - break; - case 1: - minisrv_config.config.debug_flags.debug = false; - minisrv_config.config.debug_flags.quiet = true; - minisrv_config.config.debug_flags.show_headers = true; - if (notices) console.log(" * Console Verbosity level 1 (headers shown)") - break; - case 2: - minisrv_config.config.debug_flags.debug = true; - minisrv_config.config.debug_flags.quiet = true; - minisrv_config.config.debug_flags.show_headers = false; - if (notices) console.log(" * Console Verbosity level 2 (verbose without headers)") - break; - case 3: - minisrv_config.config.debug_flags.debug = true; - minisrv_config.config.debug_flags.quiet = true; - minisrv_config.config.debug_flags.show_headers = true; - if (notices) console.log(" * Console Verbosity level 3 (verbose with headers)") - break; - default: - minisrv_config.config.debug_flags.debug = true; - minisrv_config.config.debug_flags.quiet = false; - minisrv_config.config.debug_flags.show_headers = true; - if (notices) console.log(" * Console Verbosity level 4 (debug verbosity)") - break; - } + if (minisrv_config.config.verbosity >= 0 && minisrv_config.config.verbosity <= 3) { + debugFlags.quiet = minisrv_config.config.verbosity < 2; + debugFlags.show_headers = minisrv_config.config.verbosity % 2 === 1; + debugFlags.debug = minisrv_config.config.verbosity === 2 || minisrv_config.config.verbosity === 3; + log(` * Console Verbosity level ${minisrv_config.config.verbosity}`); + } else { + Object.assign(debugFlags, { debug: true, quiet: false, show_headers: true }); + log(" * Console Verbosity level 4 (debug verbosity)"); } - if (notices || reload_notice) console.log(" *** Configuration successfully read."); + minisrv_config.config.debug_flags = debugFlags; + + log(" *** Configuration successfully read."); this.minisrv_config = minisrv_config; return this.minisrv_config; +} + + integrateConfig(main, user) { + for (const key in user) { + if (typeof user[key] === 'object' && user[key] !== null && !Array.isArray(user[key])) { + main[key] = this.integrateConfig(main[key] || {}, user[key]); + } else { + main[key] = user[key]; + } + } + return main; } + writeToUserConfig(config) { if (config) { try { @@ -686,18 +626,34 @@ class WTVShared { * @param {string} extra_chars String of extra characters outside A-Z a-z 0-9 to include * @returns {string} Random string */ - generateString(len, extra_chars = null) { - var result = ''; - var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - if (extra_chars) characters += extra_chars; - var charactersLength = characters.length; - for (var i = 0; i < len; i++) { - result += characters.charAt(Math.floor(Math.random() * - charactersLength)); + generateString(len, extra_chars = '') { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + extra_chars; + const charactersLength = characters.length; + let result = ''; + let randomByte; + let value; + + // If in Node.js environment, use crypto for better performance and randomness + if (typeof require === 'function' && typeof process === 'object') { + const { randomBytes } = require('crypto'); + for (let i = 0; i < len; i++) { + randomByte = randomBytes(1); + result += characters.charAt(randomByte[0] % charactersLength); + } + } else { + // Cache the Math functions outside of the loop + const randomFunc = Math.random; + const floorFunc = Math.floor; + for (let i = 0; i < len; i++) { + value = floorFunc(randomFunc() * charactersLength); + result += characters.charAt(value); + } } + return result; } + /** * Any alias of generateString with optional special characters enabled as well * @param {string} len desired generated string length @@ -718,24 +674,20 @@ class WTVShared { lineWrap(string, len = 72, join = "\n") { if (string.length <= len) return string; - var split; - if (string.match(" ")) { - // split if text with space, respecting words - split = string.match(new RegExp('([\\s\\S]){1,' + len + '}?!\\S', "g")); + // Create the regex outside of the loop to avoid recompilation + const wordWrapRegex = new RegExp(`(.{1,${len}})(\\s|$)|(.{${len}})`, 'g'); + const matches = []; + let match; + + while ((match = wordWrapRegex.exec(string)) !== null) { + // Add the matched group that is not undefined + matches.push(match[1] || match[3]); } - if (!split) { - // fallback if above failed, or if its just a really long word (eg base64) - split = string.match(new RegExp('.{1,' + len + '}', "g")); - } else Object.keys(split).forEach((k) => { - if (split[k].substr(0, 1) == ' ') split[k] = split[k].trim(' '); - }); - if (split) return split.join(join); - else return null; + return matches.join(join).trim(); } - /** * Returns the Last-Modified date in Unix Timestamp format * @param {string} file Path to a file @@ -767,40 +719,68 @@ class WTVShared { * Converts a binary buffer to a urlencoded string * @param {ArrayBuffer} buf byte data array */ - urlEncodeBytes = (buf) => { - let encoded = '' + urlEncodeBytes(buf) { + if (!(buf instanceof Uint8Array)) { + buf = new Uint8Array(buf); + } + + const urlSafeChars = /[A-Za-z0-9_.~-]/; + let encoded = ''; + let hex; + for (let i = 0; i < buf.length; i++) { - const charBuf = Buffer.from('00', 'hex') - charBuf.writeUInt8(buf[i]) - const char = charBuf.toString() - // if the character is safe, then just print it, otherwise encode - if (isUrlSafe(char)) { - encoded += char + const byte = buf[i]; + // Check if the byte maps to a URL-safe character + if (urlSafeChars.test(String.fromCharCode(byte))) { + encoded += String.fromCharCode(byte); } else { - encoded += `%${charBuf.toString('hex').toUpperCase()}` + // Convert byte to a two-character hexadecimal code + hex = byte.toString(16); + encoded += `%${hex.length === 1 ? '0' + hex : hex}`; } } - return encoded + + return encoded.toUpperCase(); } + /** * Decodes a urlencoded string into a binary buffer * @param {string} encoded urlencoded string */ - urlDecodeBytes = (encoded) => { - let decoded = Buffer.from('') + urlDecodeBytes(encoded) { + // Calculate the length of the decoded buffer + let bufferLength = encoded.length; for (let i = 0; i < encoded.length; i++) { if (encoded[i] === '%') { - const charBuf = Buffer.from(`${encoded[i + 1]}${encoded[i + 2]}`, 'hex') - decoded = Buffer.concat([decoded, charBuf]) - i += 2 - } else { - const charBuf = Buffer.from(encoded[i]) - decoded = Buffer.concat([decoded, charBuf]) + bufferLength -= 2; // Each encoded sequence is three characters but represents one byte + i += 2; // Skip the next two characters } - } - return decoded + + // Allocate a buffer of the correct size + let decoded = Buffer.alloc(bufferLength); + let bufferIndex = 0; + + for (let i = 0; i < encoded.length; i++) { + if (encoded[i] === '%') { + decoded[bufferIndex++] = parseInt(encoded.substr(i + 1, 2), 16); + i += 2; // Skip the next two characters + } else { + decoded[bufferIndex++] = encoded.charCodeAt(i); + } + } + + return decoded; + } + + censorSSID(ssid) { + if (ssid.slice(0, 8) === "MSTVSIMU") { + return ssid.slice(0, 10) + ('*').repeat(10) + ssid.slice(20); + } else if (ssid.slice(0, 5) === "1SEGA") { + return ssid.slice(0, 6) + ('*').repeat(6) + ssid.slice(12); + } + return ssid.slice(0, 6) + ('*').repeat(9); } /** @@ -808,108 +788,84 @@ class WTVShared { * @param {string|Array} obj SSID String or Headers Object */ filterSSID(obj) { - if (this.minisrv_config.config.hide_ssid_in_logs === true) { - if (typeof (obj) == "string") { - if (obj.substr(0, 8) == "MSTVSIMU") { - return obj.substr(0, 10) + ('*').repeat(10) + obj.substr(20); - } else if (obj.substr(0, 5) == "1SEGA") { - return obj.substr(0, 6) + ('*').repeat(6) + obj.substr(13); - } else { - return obj.substr(0, 6) + ('*').repeat(9); - } - } else { - var newobj = this.cloneObj(obj); - if (obj.post_data) newobj.post_data = obj.post_data; - if (newobj["wtv-client-serial-number"]) { - var ssid = newobj["wtv-client-serial-number"]; - if (ssid.substr(0, 8) == "MSTVSIMU") { - newobj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20); - } else if (ssid.substr(0, 5) == "1SEGA") { - newobj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13); - } else { - newobj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9); - } - } - return newobj; - } - } else { - return obj; - } - } - - filterRequestLog(obj) { - if (this.minisrv_config.config.filter_passwords_in_logs === true) { - if (obj.query) { - var newobj = this.cloneObj(obj); - try { - Object.keys(obj.query).forEach(function (k) { - var key = k.toLowerCase(); - switch (true) { - case /passw(or)?d/.test(key): - case /^pass$/.test(key): - newobj.query[key] = ('*').repeat(newobj.query[key].length); - break; - } - }); - return newobj; - } catch (e) { - if (!this.minisrv_config.config.debug_flags.quiet) console.error(' *** error filtering logs', e); - return obj; + var new_obj = false; + if (this.minisrv_config && this.minisrv_config.config.hide_ssid_in_logs) { + if (typeof obj === "string") { + return this.censorSSID(obj); + } else if (typeof obj === "object" && obj !== null) { + if ("wtv-client-serial-number" in obj) { + new_obj = this.cloneObj(obj) + new_obj["wtv-client-serial-number"] = this.censorSSID(new_obj["wtv-client-serial-number"]); } + // Assuming cloneObj is necessary for other reasons + return this.cloneObj((new_obj != false) ? new_obj : obj); } } return obj; } + + filterRequestLog(obj) { + if (this.minisrv_config.config.filter_passwords_in_logs && obj.query) { + const passwordRegex = /(^pass$|passw(or)?d)/i; + let newobj = this.cloneObj(obj); // Clone the object once at the beginning + + Object.keys(newobj.query).forEach((k) => { + if (passwordRegex.test(k)) { + newobj.query[k] = '*'.repeat(newobj.query[k].length); + } + }); + + return newobj; + } + + return obj; + } + + + decodePostData(obj) { if (obj.post_data) { - if (this.minisrv_config.config.filter_passwords_in_logs === true) { - // complex, to filter - var post_obj = {}; - post_obj.query = []; - try { - var post_text = obj.post_data.toString(CryptoJS.enc.Utf8); - if (post_text.length > 0) { - post_text = post_text.split("&"); - for (let i = 0; i < post_text.length; i++) { - var qraw_split = post_text[i].split("="); - if (qraw_split.length == 2) { - var k = qraw_split[0]; - post_obj.query[k] = unescape(post_text[i].split("=")[1].replace(/\+/g, "%20")); - } + const filterPasswords = this.minisrv_config.config.filter_passwords_in_logs === true; + try { + // Assuming CryptoJS.enc.Utf8 exists and has a stringify method + let post_text = CryptoJS.enc.Utf8.stringify(obj.post_data); + let params = new URLSearchParams(post_text); + + if (filterPasswords) { + for (let [key, value] of params) { + const lowerKey = key.toLowerCase(); + if (/passw(or)?d|^pass$/.test(lowerKey)) { + params.set(key, '*'.repeat(value.length)); } } - var post_obj = this.filterRequestLog(post_obj); - post_text = ""; - Object.keys(post_obj.query).forEach(function (k) { - post_text += k + "=" + post_obj.query[k] + "&"; - }); - post_text = post_text.substring(0, post_text.length - 1); - obj.post_data = post_text.hexEncode(); - } catch (e) { - obj.post_data = obj.post_data.toString(CryptoJS.enc.Hex); } - } else { - // simple, no filter - obj.post_data = obj.post_data.toString(); + + // Assuming hexEncode method exists on the string prototype + obj.post_data = filterPasswords ? params.toString().hexEncode() : params.toString(); + } catch (e) { + // Fallback in case of an error + if (!this.minisrv_config.config.debug_flags.quiet) { + console.error(' *** error decoding post data', e); + } + // Assuming CryptoJS.enc.Hex exists and has a stringify method + obj.post_data = CryptoJS.enc.Hex.stringify(obj.post_data); } } return obj; } + // DON'T USE THIS // Saved for reference until I come up with a better way // If used, this will exceed the stack limit over time unloadModule(moduleName) { - // for handling template classes - var solvedName = require.resolve(moduleName), - nodeModule = require.cache[solvedName]; - if (nodeModule) { - for (var i = 0; i < nodeModule.children.length; i++) { - var child = nodeModule.children[i]; - this.unloadModule(child.filename); - } - delete require.cache[solvedName]; + // Search for the module in the require cache + let resolvedPath = require.resolve(moduleName); + + // Remove the module from the cache + if (require.cache[resolvedPath]) { + delete require.cache[resolvedPath]; } } @@ -918,19 +874,30 @@ class WTVShared { * @param {string} path * @param {string} directory Root directory */ - getAbsolutePath(path, directory = null) { - if (directory) { - if (path.indexOf(directory) == -1) { - directory = this.getAbsolutePath(directory); - try { - if (this.fs.lstatSync(directory).isDirectory()) directory = directory + this.path.sep; - } catch (e) { } - path = directory + path; + getAbsolutePath(path, directory = '') { + const pathModule = require('path'); + try { + // If the directory is a valid directory, prepend it to the path + if (directory && !path.startsWith(directory)) { + const fs = require('fs'); + if (fs.lstatSync(directory).isDirectory()) { + // Ensure directory has trailing separator + if (!directory.endsWith(pathModule.sep)) { + directory += pathModule.sep; + } + path = directory + path; + } } + } catch (e) { + // If there's an error accessing the directory, log it or handle as needed + console.error('Error resolving directory:', e); } - return this.fixPathSlashes(this.path.resolve(path)); + // The path.resolve method will take care of normalizing slashes + return pathModule.resolve(path); } + + /** * Returns a percentage * @param {number} partialValue @@ -961,38 +928,37 @@ class WTVShared { return path.reverse().split(".")[0].reverse(); } - getLineFromFile(filename, line_no, callback) { - var stream = this.fs.createReadStream(filename, { - flags: 'r', - encoding: 'utf-8', - fd: null, - bufferSize: 64 * 1024 + getLineFromFile(filename, lineNo, callback) { + let lineCount = 0; + const lineReader = this.readline.createInterface({ + input: this.fs.createReadStream(filename, { + flags: 'r', + encoding: 'utf-8' + }), + crlfDelay: Infinity }); - - var fileData = ''; - stream.on('data', function (data) { - fileData += data; - - // The next lines should be improved - var lines = fileData.split("\n"); - - if (lines.length >= +line_no) { - stream.destroy(); - callback(null, lines[+line_no]); + lineReader.on('line', (line) => { + lineCount++; + if (lineCount === lineNo) { + lineReader.close(); + callback(null, line); } }); - stream.on('error', function () { - callback('Error', null); + lineReader.on('close', () => { + if (lineCount < lineNo) { + callback(new Error('File end reached without finding line'), null); + } }); - stream.on('end', function () { - callback('File end reached without finding line', null); + lineReader.on('error', (err) => { + callback(err, null); }); } + /** * Checks if service is enabled or disabled in the config * @param {string} service Service Name @@ -1046,51 +1012,31 @@ class WTVShared { * @param {boolean} pc_mode If true, sends response formatted for PCs instead of WebTV * @param {boolean} wtv_reset if true, tells the WebTV box to reset the service list and reconnect */ - doErrorPage(code, data = null, details = null, pc_mode = false, wtv_reset = false) { - var headers = null; - var minisrv_config = this.minisrv_config; - switch (code) { - case 401: - if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] }); - if (pc_mode) headers = "401 Unauthorized\n"; - else headers = code + " " + data + "\n"; - headers += "Content-Type: text/html\n"; - break; - case 403: - if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] }); - if (pc_mode) headers = "403 Forbidden\n"; - else headers = code + " " + data + "\n"; - headers += "Content-Type: text/html\n"; - break; - case 404: - if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] }); - if (pc_mode) headers = "404 Not Found\n"; - else headers = code + " " + data + "\n"; - headers += "Content-Type: text/html\n"; - break; - case 400: - case 500: - if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] }); - if (details) data += "
Details:
" + details; - if (pc_mode) headers = "500 Internal Server Error\n"; - else headers = code + " " + data + "\n"; - headers += "Content-Type: text/html\n"; - break; - default: - if (data === null && this.minisrv_config.config.errorMessages[code]) data = minisrv_config.config.errorMessages[code].replace(/\$\{(.+)\}/g, function (x) { return minisrv_config.config[x.replace("${",'').replace('}','')] }); - headers = code + " " + data + "\n"; - headers += "Content-Type: text/html\n"; - break; + doErrorPage(code, data = null, details = null, pc_mode = false, wtv_reset = false) { + const minisrv_config = this.minisrv_config; + const errorMessage = minisrv_config.config.errorMessages[code] || ""; + const message = data || errorMessage.replace(/\$\{(\w+)\}/g, (match, p1) => minisrv_config.config[p1] || ''); + + if (details && [400, 500].includes(code)) { + data += "
Details:
" + details; } + + let headers = pc_mode ? `${code} ${minisrv_config.httpStatusCodes[code]}\n` : `${code} ${message}\n`; + headers += "Content-Type: text/html\n"; + if (wtv_reset && !pc_mode) { headers += "wtv-service: reset\n"; headers += this.getServiceString('wtv-1800') + "\n"; headers += "wtv-visit: wtv-1800:/preregister?scriptless-visit-reason=999\n"; - console.error(" * doErrorPage Called (sent wtv-reset):", code, data); - } else console.error(" * doErrorPage Called:", code, data); - return new Array(headers, data); + console.error(" * doErrorPage Called (sent wtv-reset):", code, message); + } else { + console.error(" * doErrorPage Called:", code, message); + } + + return [headers, message]; } + /** * Strips bad things from paths * @param {string} base Base path @@ -1123,16 +1069,16 @@ class WTVShared { * @returns {string} corrected path */ fixPathSlashes(path) { - // fix slashes - if (this.path.sep === "/" && path.indexOf("\\") != -1) path = path.replace(/\\/g, this.path.sep); - else if (this.path.sep === "\\" && path.indexOf("/") != -1) path = path.replace(/\//g, this.path.sep); - - // remove double slashes - while (path.indexOf(this.path.sep + this.path.sep) != -1) path = path.replace(this.path.sep + this.path.sep, this.path.sep); + const pathModule = require('path'); - return path; + // Normalize the slashes to the current environment's default. + const normalizedPath = path.replace(/[\\/]+/g, pathModule.sep); + + // The path.normalize will also resolve any '..' and '.' segments. + return pathModule.normalize(normalizedPath); } + /** * Makes sure an SSID is clean, and doesn't contain any exploitable characters * @param {string} ssid