diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index bf381a90..d2f5d8cb 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -211,7 +211,7 @@ async function sendRawFile(socket, path) { var runScriptInVM = function (script_data, user_contextObj = {}, privileged = false, filename = null, debug_name = null) { // Here we define the ServiceVault Script Context Object - // The ServiceVault scripts will only be allowed to access the following fcnutions/variables. + // The ServiceVault scripts will only be allowed to access the following functions/variables. // Furthermore, only modifications to variables in `updateFromVM` will be saved. // Example: an attempt to change "minisrv_config" from a ServiceVault script would be discarded @@ -240,8 +240,8 @@ var runScriptInVM = function (script_data, user_contextObj = {}, privileged = fa "wtvmime": wtvmime, "http": http, "https": https, - "URL": URL, "sharp": sharp, + "URL": URL, "wtvshared": wtvshared, "zlib": zlib, "clientShowAlert": clientShowAlert, diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook-add.js b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook-add.js index dd6f0dd7..62d58f74 100644 --- a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook-add.js +++ b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook-add.js @@ -1,21 +1,140 @@ minisrv_service_file = true; +request_is_async = true; -if (!request_headers.query.mediaData) { - var errpage = wtvshared.doErrorPage(400, "Bad Request", "Missing mediaData parameter."); +function addToScrapbook(id, contentType, data) { + var result = session_data.addToScrapbook(id, contentType, data); + if (result) { + var successScrapbook = new clientShowAlert({ + 'image': minisrv_config.config.service_logo, + 'message': "The image has been added to your scrapbook. Would you like to view your scrapbook now?", + 'buttonlabel1': "No", + 'buttonaction1': "client:donothing", + 'buttonlabel2': "Yes", + 'buttonaction2': "wtv-author:/scrapbook", + }) + var files = session_data.listScrapbook(); + var pageNum = Math.ceil(files.length / 6); + if (pageNum > 1) { + successScrapbook.buttonaction2 += '?pageNum=' + pageNum; + } + sendToClient(socket, {'Status': 302, 'wtv-expire-all': 'wtv-author:/scrapbook', 'Location': successScrapbook.getURL()}, ''); + } else { + handleError('Failed to add image to scrapbook'); + } +} + +function handleError(reason) { + var errpage = wtvshared.doErrorPage(400, reason); + sendToClient(socket, errpage[0], errpage[1]); +} +if (!request_headers.query.mediaData && !request_headers.query.mediaPath) { + var errpage = wtvshared.doErrorPage(400, "Bad Request", "Missing mediaData or mediaPath parameter."); headers = errpage[0]; data = errpage[1]; } else { - var id = session_data.pagestore.getFreeScrapbookID(); - var result = session_data.pagestore.addToScrapbook(id, "image/jpg", request_headers.query.mediaData); - if (result) { - headers = `300 OK + const id = session_data.getFreeScrapbookID(); + if (request_headers.query.mediaPath) { + if (!request_headers.query.confirm) { + var confirmScrapbook = new clientShowAlert({ + 'image': minisrv_config.config.service_logo, + 'message': "You are about to add an image to your scrapbook.

Do you wish to continue?", + 'buttonlabel1': "Continue", + 'buttonaction1': "wtv-author:/scrapbook-add?confirm=true&mediaPath=" + encodeURIComponent(request_headers.query.mediaPath || ''), + 'buttonlabel2': "Cancel", + 'buttonaction2': "client:donothing" + }).getURL(); + sendToClient(socket, {'Status': 302, 'Location': confirmScrapbook}, ''); + } else { + function isValidImageType(contentType, url) { + // Check content-type header or file extension + if (contentType) { + return contentType === 'image/jpeg' || contentType == 'image/jpg' || contentType === 'image/gif'; + } + return url.endsWith('.jpg') || url.endsWith('.jpeg') || url.endsWith('.gif'); + } + + try { + const parsedUrl = new URL(request_headers.query.mediaPath); + const protocol = parsedUrl.protocol === 'https:' ? https : http; + + protocol.get(request_headers.query.mediaPath, (res) => { + if (res.statusCode !== 200) { + handleError('URL does not exist or returned status ' + res.statusCode); + res.resume(); + return; + } + + const contentType = res.headers['content-type']; + const contentLength = parseInt(res.headers['content-length'], 10); + + if (!isValidImageType(contentType, request_headers.query.mediaPath)) { + handleError('URL is not a JPEG or GIF image'); + res.resume(); + return; + } + + if (contentLength && contentLength > 1024 * 1024 * 4) { + handleError('Image is larger than 4MB'); + res.resume(); + return; + } + + let data = []; + let totalLength = 0; + res.on('data', (chunk) => { + totalLength += chunk.length; + if (totalLength > 1024 * 1024 * 4) { + handleError('Image is larger than 4MB'); + res.destroy(); + return; + } + data.push(chunk); + }); + + res.on('end', () => { + if (totalLength > 1024 * 1024 * 4) return; + if (totalLength > 1024 * 1024) { + sharp(Buffer.concat(data)) + .resize(640, 480, { + fit: 'inside', + withoutEnlargement: true + }) + .jpeg({ quality: 75 }) + .toBuffer() + .then(resizedBuffer => { + data = resizedBuffer; + addToScrapbook(id, "image/jpg", data); + }) + .catch(err => { + handleError('Failed to resize image'); + return; + }); + } else { + data = Buffer.concat(data); + addToScrapbook(id, contentType, data); + } + }); + + res.on('error', (err) => { + handleError('Error downloading image'); + }); + }).on('error', (err) => { + handleError('Failed to fetch URL'); + }); + } catch (e) { + handleError(e.message); + } + } + } else { + var result = session_data.addToScrapbook(id, "image/jpg", request_headers.query.mediaData); + if (result) { + headers = `300 OK Content-Type: text/html wtv-expire-all: wtv-author:/scrapbook Location: wtv-author:/scrapbook wtv-visit: wtv-author:/scrapbook`; - } else { - var errpage = wtvshared.doErrorPage(500, "Internal Server Error", "Failed to add scrapbook item."); - headers = errpage[0]; - data = errpage[1]; + } else { + handleError('Failed to add image to scrapbook'); + } } } \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook.js b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook.js index af667565..2d5102c6 100644 --- a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook.js +++ b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-author/scrapbook.js @@ -1,12 +1,14 @@ var minisrv_service_file = true; -var files = session_data.pagestore.listScrapbook(); -var dir = session_data.pagestore.scrapbookDir() -var start = 0; +var files = session_data.listScrapbook(); +var dir = session_data.scrapbookDir() +var pageNum = parseInt(request_headers.query.pageNum || 1); +var start = (pageNum - 1) * 6; headers = `200 OK Connection: Keep-Alive -Content-Type: text/html` +Content-Type: text/html +wtv-expire-all: wtv-author:/scrapbook` data = ` @@ -124,7 +126,38 @@ function StorageWarning() { } - Your scrapbook + Your scrapbook ` +if (files.length > 6) { +data += ` + + +
`; +if (pageNum > 1) { +data += ` + + @@ -207,7 +242,7 @@ data += `
` +} else { +data += `
` +} +data += ` +
+
${pageNum} of ${Math.ceil(files.length / 6)}`; +if (files.length > start + 6) { +data += ` + +
` + +} else { + data += `
` +} + data += ` +
+
` +} +data += `
' : ''}` + } +data += `
@@ -144,7 +177,7 @@ Choose Help for instructions. if (request_headers.query.addMediaURL) { data += "Choose an image to add to your web page."; } else { - data += "Choose one of your saved images to view it full size."; + data += `You are currently using ${session_data.getScrapbookUsagePercent()}% of your scrapbook storage space. Choose one of your saved images to view it full size.`; } } data += ` @@ -176,8 +209,9 @@ if (files.length > 0) { -` - for (let i = start; i < Math.min(files.length, start + 12); i++) { + +` + for (let i = start; i < Math.min(files.length, start + 6); i++) { url = "wtv-tricks:/view-scrapbook-image?id=" + files[i]; if (request_headers.query.addMediaURL) { url = unescape(request_headers.query.addMediaURL) + "&scrapbookID=" + files[i]; @@ -188,8 +222,9 @@ data += ` -${i % 4 === 1 ? '' : ''}` -data += `
+${(i - start + 1) % 3 === 0 ? '
` -} + } data += ` diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/add-to-scrapbook.js b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/add-to-scrapbook.js index f94497b4..ab928b75 100644 --- a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/add-to-scrapbook.js +++ b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/add-to-scrapbook.js @@ -7,83 +7,10 @@ function handleError(reason) { sendToClient(socket, errpage[0], errpage[1]); } -if (!request_headers.query.url) { +if (!request_headers.query.url && !request_headers.query.mediaPath) { handleError('No URL provided'); } else { - var url = request_headers.query.url; - function isValidImageType(contentType, url) { - // Check content-type header or file extension - if (contentType) { - return contentType === 'image/jpeg' || contentType == 'image/jpg' || contentType === 'image/gif'; - } - return url.endsWith('.jpg') || url.endsWith('.jpeg') || url.endsWith('.gif'); - } - - try { - const parsedUrl = new URL(url); - const protocol = parsedUrl.protocol === 'https:' ? https : http; - - protocol.get(url, (res) => { - if (res.statusCode !== 200) { - handleError('URL does not exist or returned status ' + res.statusCode); - res.resume(); - return; - } - - const contentType = res.headers['content-type']; - const contentLength = parseInt(res.headers['content-length'], 10); - - if (!isValidImageType(contentType, url)) { - handleError('URL is not a JPEG or GIF image'); - res.resume(); - return; - } - - if (contentLength && contentLength > 1024 * 1024) { - handleError('Image is larger than 1MB'); - res.resume(); - return; - } - - let data = []; - let totalLength = 0; - res.on('data', (chunk) => { - totalLength += chunk.length; - if (totalLength > 1024 * 1024) { - handleError('Image is larger than 1MB'); - res.destroy(); - return; - } - data.push(chunk); - }); - - res.on('end', () => { - if (totalLength > 1024 * 1024) return; - data = Buffer.concat(data); - var id = session_data.pagestore.getFreeScrapbookID(); - var result = session_data.pagestore.addToScrapbook(id, contentType, data); - if (result) { - var successScrapbook = new clientShowAlert({ - 'image': minisrv_config.config.service_logo, - 'message': "The image has been added to your scrapbook. Would you like to view your scrapbook now?", - 'buttonlabel1': "No", - 'buttonaction1': "client:donothing", - 'buttonlabel2': "Yes", - 'buttonaction2': "wtv-author:/scrapbook", - }).getURL(); - sendToClient(socket, {'Status': 302, 'Location': successScrapbook, 'wtv-visit': successScrapbook}, ''); - } else { - handleError('Failed to add image to scrapbook'); - } - }); - - res.on('error', (err) => { - handleError('Error downloading image'); - }); - }).on('error', (err) => { - handleError('Failed to fetch URL'); - }); - } catch (e) { - handleError(e.message); - } + var mediaURL = request_headers.query.url || request_headers.query.mediaPath; + var targetURL = 'wtv-author:/scrapbook-add?mediaPath=' + encodeURIComponent(mediaURL); + sendToClient(socket, {'Status': 302, 'Location': targetURL, 'wtv-visit': targetURL}, ''); } \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/view-scrapbook-image.js b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/view-scrapbook-image.js index 6e1ece60..087ac9aa 100644 --- a/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/view-scrapbook-image.js +++ b/zefie_wtvp_minisrv/includes/ServiceVault/wtv-tricks/view-scrapbook-image.js @@ -10,7 +10,7 @@ async function handleImage() { if (!request_headers.query.id) { handleError('No image ID specified'); } else { - data = session_data.pagestore.getScrapbookImage(request_headers.query.id); + data = session_data.getScrapbookImage(request_headers.query.id); if (!data) { handleError('Image not found'); } else { @@ -21,7 +21,7 @@ async function handleImage() { data = await sharp(data).resize({ width, withoutEnlargement: true }).toBuffer(); } headers = `200 OK -Content-Type: ${session_data.pagestore.getScrapbookImageType(request_headers.query.id)} +Content-Type: ${session_data.getScrapbookImageType(request_headers.query.id)} Content-Length: ${data.length}` sendToClient(socket, headers, data); } catch (error) { diff --git a/zefie_wtvp_minisrv/includes/classes/WTVAdmin.js b/zefie_wtvp_minisrv/includes/classes/WTVAdmin.js index 600ac709..e64d3055 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVAdmin.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVAdmin.js @@ -1,3 +1,5 @@ +const WTVClientSessionData = require('./WTVClientSessionData.js'); + class WTVAdmin { fs = require('fs'); @@ -18,6 +20,12 @@ class WTVAdmin { REASON_EXISTS = 4 REASON_NONEXIST = 5 + /** + * Creates an instance of WTVAdmin. + * @param {Object} minisrv_config + * @param {WTVClientSessionData} wtvclient + * @param {string} service_name + */ constructor(minisrv_config, wtvclient, service_name) { this.minisrv_config = minisrv_config; var { WTVShared } = require("./WTVShared.js"); @@ -36,6 +44,12 @@ class WTVAdmin { this.service_name = service_name; } + /** + * Bans a specific SSID. + * @param {string} ssid The SSID to ban + * @param {string} admin_ssid The SSID of the admin requesting the ban + * @returns {number} The result of the ban operation + */ banSSID(ssid, admin_ssid = null) { if (ssid == admin_ssid) { return this.REASON_NOSELF; @@ -58,22 +72,27 @@ class WTVAdmin { } } + /** + * Unbans a specific SSID. + * @param {string} ssid The SSID to unban + * @returns {number} The result of the unban operation + */ unbanSSID(ssid) { var config_changed = false; - var fake_config = wtvshared.getUserConfig(); + var fake_config = this.wtvshared.getUserConfig(); if (!fake_config.config) fake_config.config = {}; if (!fake_config.config.ssid_block_list) fake_config.config.ssid_block_list = []; - if (typeof request_headers.query.ssid === 'string') { + if (typeof ssid === 'string') { Object.keys(fake_config.config.ssid_block_list).forEach(function (k) { - if (fake_config.config.ssid_block_list[k].toLowerCase() == request_headers.query.ssid.toLowerCase()) { + if (fake_config.config.ssid_block_list[k].toLowerCase() == ssid.toLowerCase()) { fake_config.config.ssid_block_list.splice(k, 1); config_changed = true } }); } else { Object.keys(fake_config.config.ssid_block_list).forEach(function (k) { - Object.keys(request_headers.query.ssid).forEach(function (j) { - if (fake_config.config.ssid_block_list[k].toLowerCase() == request_headers.query.ssid[j].toLowerCase()) { + Object.keys(ssid).forEach(function (j) { + if (fake_config.config.ssid_block_list[k].toLowerCase() == ssid[j].toLowerCase()) { fake_config.config.ssid_block_list.splice(k, 1); config_changed = true } @@ -89,38 +108,11 @@ class WTVAdmin { } } - ip2long(ip) { - var components; - - if (components = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) { - var iplong = 0; - var power = 1; - for (var i = 4; i >= 1; i -= 1) { - iplong += power * parseInt(components[i]); - power *= 256; - } - return iplong; - } - else return -1; - } - - isInSubnet(ip, subnet) { - // If subnet is given as a single IP address (no CIDR notation) - if (subnet.indexOf('/') === -1) { - return this.ip2long(ip) === this.ip2long(subnet); - } else { - // Expect subnet in format "base_ip/prefix_length" - let parts = subnet.match(/^(.*?)\/(\d{1,2})$/); - if (parts && (this.ip2long(parts[1]) >= 0)) { - let base_ip = this.ip2long(parts[1]); - let prefixLength = parseInt(parts[2]); - let freedom = Math.pow(2, 32 - prefixLength); - return (this.ip2long(ip) >= base_ip) && (this.ip2long(ip) < base_ip + freedom); - } - } - return false; - } - + /** + * Rejects a connection attempt based on the client's address or SSID. + * @param {boolean} reason_is_ssid If true, the rejection is based on SSID, otherwise on IP address + * @returns {string} The reason for rejecting the connection + */ rejectConnection(reason_is_ssid) { var rejectReason; if (this.pcservices) { @@ -138,6 +130,11 @@ class WTVAdmin { return rejectReason; } + /** + * Checks if the provided password matches the service's password. + * @param {string} password The password to check + * @returns {boolean} True if the password matches, false otherwise + */ checkPassword(password) { if (this.pcservices) { if (this.minisrv_config.config.pc_admin.password) { @@ -156,6 +153,10 @@ class WTVAdmin { } } + /** + * Lists all registered SSIDs. + * @returns {Array} An array of arrays, each containing the SSID and its associated account information + */ listRegisteredSSIDs() { var search_dir = this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore + this.path.sep + "accounts"); var self = this; @@ -169,6 +170,11 @@ class WTVAdmin { return out; } + /** + * Checks if the current client is authorized to access the service. + * @param {boolean} justchecking If true, only checks authorization without rejecting the connection + * @return {boolean} True if authorized, false otherwise + */ isAuthorized(justchecking = false) { var allowed_ssid = false; var allowed_ip = false; @@ -186,7 +192,7 @@ class WTVAdmin { if (ssid == self.wtvclient.ssid) { allowed_ssid = true; Object.keys(self.minisrv_config.services[self.service_name].authorized_ssids[k]).forEach(function (j) { - if (self.isInSubnet(self.clientAddress, self.minisrv_config.services[self.service_name].authorized_ssids[k][j])) { + if (self.wtvshared.isInSubnet(self.clientAddress, self.minisrv_config.services[self.service_name].authorized_ssids[k][j])) { if (allowed_ip) return; allowed_ip = true; } @@ -206,7 +212,7 @@ class WTVAdmin { var self = this; Object.keys(this.minisrv_config.config.pc_admin.ip_whitelist).forEach(function (k) { if (allowed_ip) return; - allowed_ip = self.isInSubnet(self.clientAddress, self.minisrv_config.config.pc_admin.ip_whitelist[k]); + allowed_ip = self.wtvshared.isInSubnet(self.clientAddress, self.minisrv_config.config.pc_admin.ip_whitelist[k]); }); } } @@ -219,6 +225,12 @@ class WTVAdmin { } } + /** + * Gets the account information for a specific username. + * @param {string} username The username to get the account information for + * @param {string|null} directory The directory to search for user accounts, defaults to the session store directory + * @returns {Object|null} An object containing account information if the username is found, null otherwise + */ getAccountInfo(username, directory = null) { var search_dir = this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore + this.path.sep + "accounts"); var account_data = null; @@ -255,6 +267,11 @@ class WTVAdmin { return null; } + /** + * Gets the account information for a specific SSID. + * @param {string} ssid The SSID to get the account information for + * @returns {Object|boolean} An object containing account information if the SSID is registered, false otherwise + */ getAccountInfoBySSID(ssid) { var account_info = {}; var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid); @@ -278,13 +295,22 @@ class WTVAdmin { else return false; } - + /** + * Gets the account session data for a specific SSID. + * @param {string} ssid The SSID to get the account data for + * @returns {WTVClientSessionData} The session data object for the account + */ getAccountBySSID(ssid) { var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid); userSession.user_id = 0; return userSession; } + /** + * Checks if a specific SSID is banned. + * @param {string} ssid The SSID to check + * @returns {boolean} True if the SSID is banned, false otherwise + */ isBanned(ssid) { var self = this; var isBanned = false; diff --git a/zefie_wtvp_minisrv/includes/classes/WTVAuthor.js b/zefie_wtvp_minisrv/includes/classes/WTVAuthor.js index abb87738..a4b0bebe 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVAuthor.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVAuthor.js @@ -10,7 +10,6 @@ class WTVAuthor { wtvclient = null; pageFileExt = ".page"; pagestore_dir = null; - scrapbook_dir = null; pageArr = []; blockArr = []; header = null; @@ -68,18 +67,6 @@ class WTVAuthor { } return true; } - - scrapbookExists() { - if (!this.isguest) { - if (this.scrapbook_dir === null) { - var userstore_dir = this.wtvclient.getUserStoreDirectory(); - var store_dir = "Scrapbook" + this.path.sep; - this.scrapbook_dir = userstore_dir + store_dir; - } - } - return this.fs.existsSync(this.scrapbook_dir); - } - createPagestore() { if (this.pagestoreExists() === false) { @@ -91,92 +78,6 @@ class WTVAuthor { return false; } - createScrapbook() { - if (this.scrapbookExists() === false) { - try { - if (!this.fs.existsSync(this.scrapbook_dir)) this.fs.mkdirSync(this.scrapbook_dir, { recursive: true }); - return true; - } catch { } - } - return false - } - - scrapbookDir() { - if (this.scrapbookExists() === false) { - this.createScrapbook(); - } - return this.scrapbook_dir; - } - - listScrapbook() { - if (this.scrapbookExists() === false) { - this.createScrapbook(); - } - const files = this.fs.readdirSync(this.scrapbook_dir); - const filteredFiles = files.filter(file => !file.endsWith('.meta')); - return filteredFiles; - } - - getFreeScrapbookID() { - if (this.scrapbookExists() === false) { - this.createScrapbook(); - } - var id = 1; - var files = this.fs.readdirSync(this.scrapbook_dir); - if (files.length == 0) { - return id; - } - files = files.map(file => parseInt(file.substr(0, file.indexOf('.')))); - while (files.includes(id)) { - id++; - } - return id; - } - - getScrapbookImage(id) { - if (this.scrapbookExists() === false) { - this.createScrapbook(); - } - var file = this.scrapbook_dir + id; - if (this.fs.existsSync(file)) { - return this.fs.readFileSync(file); - } - return null; - } - - getScrapbookImageType(id) { - if (this.scrapbookExists() === false) { - this.createScrapbook(); - } - var file = this.scrapbook_dir + id + ".meta"; - if (this.fs.existsSync(file)) { - var meta = this.fs.readFileSync(file, 'utf8'); - try { - var metaData = JSON.parse(meta); - return metaData.contentType; - } catch (e) { - this.debug("getScrapbookImageType", "Error parsing metadata for image ID", id, e); - } - } - return null; - } - - addToScrapbook(filename, contentType, data) { - try { - if (this.scrapbookExists() === false) { - this.createScrapbook(); - } - var fileout = this.scrapbook_dir + filename; - var fileout_meta = this.scrapbook_dir + filename + ".meta"; - this.fs.writeFileSync(fileout, data); - this.fs.writeFileSync(fileout_meta, JSON.stringify({ - "contentType": contentType - })); - return true; - } catch {} - return false; - } - createPage(style) { this.pagestoreExists() var pagestorepath = this.pagestore_dir; diff --git a/zefie_wtvp_minisrv/includes/classes/WTVClientSessionData.js b/zefie_wtvp_minisrv/includes/classes/WTVClientSessionData.js index 155fda22..5bccc731 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVClientSessionData.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVClientSessionData.js @@ -16,6 +16,7 @@ class WTVClientSessionData { mailstore = null; favstore = null; pagestore = null; + scrapbook_dir = null; login_security = null; capabilities = null; session_storage = ""; @@ -108,6 +109,12 @@ class WTVClientSessionData { } } + /** + * Sets a ticket data value. + * @param {string} key The key of the ticket data + * @param {*} value The value to set + * @returns {boolean} True if the value was set successfully, false otherwise + */ setTicketData(key, value) { if (this.data_store.wtvsec_login) this.data_store.wtvsec_login.setTicketData(key, value); else return false; @@ -115,16 +122,24 @@ class WTVClientSessionData { return true; } + /** + * Retrieves ticket data by key. + * @param {string} key The key of the ticket data + * @return {*} The value associated with the key, or false if not found + */ getTicketData(key) { if (this.data_store.wtvsec_login) return this.data_store.wtvsec_login.getTicketData(key); - return false; } + /** + * Deletes ticket data by key. + * @param {string} key The key of the ticket data to delete + * @return {boolean} True if the data was deleted successfully, false otherwise + */ deleteTicketData(key) { if (this.data_store.wtvsec_login) this.data_store.wtvsec_login.deleteTicketData(key); else return false; - return true; } @@ -328,6 +343,136 @@ class WTVClientSessionData { return (result === false) ? false : true; } + scrapbookExists() { + if (!this.isguest) { + if (this.scrapbook_dir === null) { + var userstore_dir = this.getUserStoreDirectory(); + var store_dir = "Scrapbook" + this.path.sep; + this.scrapbook_dir = userstore_dir + store_dir; + } + } + return this.fs.existsSync(this.scrapbook_dir); + } + + createScrapbook() { + if (this.scrapbookExists() === false) { + try { + if (!this.fs.existsSync(this.scrapbook_dir)) this.fs.mkdirSync(this.scrapbook_dir, { recursive: true }); + return true; + } catch { } + } + return false + } + + scrapbookDir() { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + return this.scrapbook_dir; + } + + listScrapbook() { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + const files = this.fs.readdirSync(this.scrapbook_dir); + const filteredFiles = files.sort(function(a, b) { + return a.localeCompare(b, undefined, { + numeric: true, + sensitivity: 'base' + }); + }).filter(file => !file.endsWith('.meta')); + return filteredFiles; + } + + getFreeScrapbookID() { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + var id = 1; + var files = this.fs.readdirSync(this.scrapbook_dir); + if (files.length == 0) { + return id; + } + files = files.map(file => parseInt(file.substr(0, file.indexOf('.')))); + while (files.includes(id)) { + id++; + } + return id; + } + + getScrapbookUsage() { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + var total_size = 0; + var files = this.fs.readdirSync(this.scrapbook_dir); + files.forEach(file => { + if (!file.endsWith('.meta')) { + var file_path = this.scrapbook_dir + file; + if (this.fs.existsSync(file_path)) { + total_size += this.fs.statSync(file_path).size; + } + } + }); + return total_size; + } + + getScrapbookUsagePercent() { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + var total_size = this.getScrapbookUsage(); + var max_size = this.minisrv_config.config.user_accounts.scrapbook_storage * 1024 * 1024; // convert to bytes + if (max_size <= 0) return 0; // no storage limit set + var usage_percent = (total_size / max_size) * 100; + return Math.round(usage_percent, 2); + } + + getScrapbookImage(id) { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + var file = this.scrapbook_dir + id; + if (this.fs.existsSync(file)) { + return this.fs.readFileSync(file); + } + return null; + } + + getScrapbookImageType(id) { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + var file = this.scrapbook_dir + id + ".meta"; + if (this.fs.existsSync(file)) { + var meta = this.fs.readFileSync(file, 'utf8'); + try { + var metaData = JSON.parse(meta); + return metaData.contentType; + } catch (e) { + this.debug("getScrapbookImageType", "Error parsing metadata for image ID", id, e); + } + } + return null; + } + + addToScrapbook(filename, contentType, data) { + try { + if (this.scrapbookExists() === false) { + this.createScrapbook(); + } + var fileout = this.scrapbook_dir + filename; + var fileout_meta = this.scrapbook_dir + filename + ".meta"; + this.fs.writeFileSync(fileout, data); + this.fs.writeFileSync(fileout_meta, JSON.stringify({ + "contentType": contentType + })); + return true; + } catch {} + return false; + } + /** * Retrieves a file from the user store * @param {string} path Path relative to the User File Store @@ -373,12 +518,23 @@ class WTVClientSessionData { return Object.keys(this.session_store.cookies).length || 0; } + /** + * Resets the user cookies + */ resetCookies() { this.session_store.cookies = {}; // webtv likes to have at least one cookie in the list, set a dummy cookie for zefie's site expiring in 1 year. this.addCookie("wtv.zefie.com", "/", this.wtvshared.getUTCTime(365 * 86400000), "cookie_type=chocolatechip"); } + /** + * Adds a cookie to the user's session store + * @param {string|object} domain Domain for the cookie, or an object with cookie data + * @param {string|null} path Path for the cookie, defaults to null + * @param {string|null} expires Expiration date for the cookie, defaults to null + * @param {string|null} data Data for the cookie, defaults to null + * @return {boolean} True if the cookie was added successfully, false otherwise + */ addCookie(domain, path = null, expires = null, data = null) { if (!this.checkCookies()) this.resetCookies(); if (!domain) return false; @@ -413,6 +569,12 @@ class WTVClientSessionData { return true; } + /** + * Retrieves a cookie from the user's session store + * @param {string} domain Domain of the cookie + * @param {string} path Path of the cookie + * @return {object|false} Cookie data if found, false otherwise + */ getCookie(domain, path) { if (!this.checkCookies()) this.resetCookies(); var self = this; @@ -431,6 +593,12 @@ class WTVClientSessionData { return result; } + /** + * Retrieves a cookie string from the user's session store + * @param {string} domain Domain of the cookie + * @param {string} path Path of the cookie + * @return {string|false} Cookie string if found, false otherwise + */ getCookieString(domain, path) { var cookie_data = this.getCookie(domain, path); /* @@ -443,6 +611,12 @@ class WTVClientSessionData { return cookie_data.cookie; } + /** + * Deletes a cookie from the user's session store + * @param {string|object} domain Domain of the cookie, or an object with cookie data + * @param {string|null} path Path of the cookie, defaults to null + * @return {boolean} True if the cookie was deleted successfully, false otherwise + */ deleteCookie(domain, path = null) { var result = false; if (!this.checkCookies()) { @@ -472,12 +646,20 @@ class WTVClientSessionData { return result; } + /** + * Checks if there are any cookies stored in the session + * @return {boolean} True if there are cookies, false otherwise + */ checkCookies() { if (!this.session_store.cookies) return false; else if (this.session_store.cookies == []) return false; return true; } + /** + * Lists all cookies in the user's session store + * @return {string} String representation of all cookies, each cookie separated by a null character + */ listCookies() { if (!this.checkCookies()) this.resetCookies(); var outstring = ""; diff --git a/zefie_wtvp_minisrv/includes/classes/WTVPCAdmin.js b/zefie_wtvp_minisrv/includes/classes/WTVPCAdmin.js deleted file mode 100644 index c1705649..00000000 --- a/zefie_wtvp_minisrv/includes/classes/WTVPCAdmin.js +++ /dev/null @@ -1,177 +0,0 @@ -class WTVPCAdmin { - - fs = require('fs'); - path = require('path'); - minisrv_config = []; - wtvr = null; - wtvshared = null; - socket = null; - WTVClientSessionData = require("./WTVClientSessionData.js"); - service_name = "wtv-admin"; - - constructor(minisrv_config, socket, service_name) { - this.minisrv_config = minisrv_config; - var { WTVShared } = require("./WTVShared.js"); - var WTVRegister = require("./WTVRegister.js"); - this.socket = socket; - this.wtvr = new WTVRegister(minisrv_config); - this.clientAddress = socket.remoteAddress; - this.service_name = service_name; - } - - - ip2long(ip) { - var components; - - if (components = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) { - var iplong = 0; - var power = 1; - for (var i = 4; i >= 1; i -= 1) { - iplong += power * parseInt(components[i]); - power *= 256; - } - return iplong; - } - else return -1; - } - - isInSubnet(ip, subnet) { - if (subnet.indexOf('/') == -1) { - var mask, base_ip, long_ip = this.ip2long(ip); - var mask2, base_ip2, long_ip2 = this.ip2long(ip); - return (long_ip == long_ip2); - } else { - var mask, base_ip, long_ip = this.ip2long(ip); - if ((mask = subnet.match(/^(.*?)\/(\d{1,2})$/)) && ((base_ip = this.ip2long(mask[1])) >= 0)) { - var freedom = Math.pow(2, 32 - parseInt(mask[2])); - return (long_ip > base_ip) && (long_ip < base_ip + freedom - 1); - } - } - return false; - } - - rejectConnection() { - var rejectReason; - rejectReason = this.clientAddress + " is not in the whitelist."; - console.log(" * Request from IP", this.clientAddress, "for PC Services Admin, but that IP is not authorized."); - return rejectReason; - } - - checkPassword(password) { - if (this.minisrv_config.config.pc_admin.password) { - return (password == this.minisrv_config.config.pc_admin.password); - } else { - // no password set - return true; - } - } - - isAuthorized(justchecking = false) { - var allowed_ip = false; - if (this.minisrv_config.config.pc_admin.ip_whitelist) { - var self = this; - Object.keys(this.minisrv_config.config.pc_admin.ip_whitelist).forEach(function (k) { - allowed_ip = self.isInSubnet(self.clientAddress, self.minisrv_config.config.pc_admin.ip_whitelist[k]); - }); - } - if (justchecking) { - return allowed_ip; - } else { - return allowed_ip ? true : this.rejectConnection(); - } - } - - listRegisteredSSIDs() { - var search_dir = this.minisrv_config.config.SessionStore + this.path.sep + "accounts"; - var self = this; - var out = []; - this.fs.readdirSync(search_dir).forEach(file => { - if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory()) { - var user = self.getAccountInfoBySSID(file); - out.push([file,user]); - } - }); - return out; - } - - getAccountInfo(username, directory = null) { - var search_dir = this.minisrv_config.config.SessionStore + this.path.sep + "accounts"; - var account_data = null; - var self = this; - if (directory) search_dir = directory; - this.fs.readdirSync(search_dir).forEach(file => { - if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && account_data === null) { - account_data = self.getAccountInfo(username, search_dir + self.path.sep + file); - } - if (account_data !== null) return; - if (!file.match(/.*\.json/ig)) return; - try { - var temp_session_data_file = self.fs.readFileSync(search_dir + self.path.sep + file, 'Utf8'); - var temp_session_data = JSON.parse(temp_session_data_file); - - if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) { - account_data = [temp_session_data, (search_dir + self.path.sep + file).replace(this.minisrv_config.config.SessionStore + this.path.sep + "accounts", "").split(this.path.sep)[1]]; - } - } catch (e) { - console.error(" # Error parsing Session Data JSON", search_dir + self.path.sep + file, e); - } - }); - if (account_data !== null) { - if (account_data.ssid) return account_data; - var account_info = {}; - account_info.ssid = account_data[1]; - account_info.username = account_data[0].subscriber_username; - account_info.user_id = account_data[0].subscriber_userid; - var userSession = new this.WTVClientSessionData(this.minisrv_config, account_info.ssid); - userSession.user_id = 0; - account_info.account_users = userSession.listPrimaryAccountUsers(); - return account_info; - } - return null; - } - - getAccountInfoBySSID(ssid) { - var account_info = {}; - var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid); - userSession.user_id = 0; - if (userSession.isRegistered(false)) { - account_info.ssid = ssid; - account_info.account_users = userSession.listPrimaryAccountUsers(); - if (account_info.account_users) { - if (account_info.account_users['subscriber']) { - account_info.username = account_info.account_users['subscriber'].subscriber_username; - } else { - account_info.username = account_info.account_users[0]; - } - } else { - account_info.username = account_info.account_users[0]; - } - - account_info.user_id = 0; - return account_info; - } - else return false; - } - - - getAccountBySSID(ssid) { - var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid); - userSession.user_id = 0; - return userSession; - } - - isBanned(ssid) { - var self = this; - var isBanned = false; - if (this.minisrv_config.config.ssid_block_list) { - Object.keys(this.minisrv_config.config.ssid_block_list).forEach(function (k) { - if (self.minisrv_config.config.ssid_block_list[k] == ssid) { - isBanned = true; - } - }); - } - return isBanned; - } -} - -module.exports = WTVPCAdmin; diff --git a/zefie_wtvp_minisrv/includes/classes/WTVRegister.js b/zefie_wtvp_minisrv/includes/classes/WTVRegister.js index 3ad0192b..70fde42c 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVRegister.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVRegister.js @@ -21,12 +21,22 @@ class WTVRegister { } } + /** + * Checks if the username is valid according to the configured rules. + * @param {string} username The username to check + * @returns {boolean} True if the username is valid, false otherwise + */ checkUsernameSanity(username) { var regex_str = "^([A-Za-z0-9-\_]{" + this.minisrv_config.config.user_accounts.min_username_length + "," + this.minisrv_config.config.user_accounts.max_username_length + "})$"; var regex = new RegExp(regex_str); return regex.test(username); } + /** + * Checks if the SSID is already registered. + * @param {string} ssid The SSID to check + * @returns {boolean} True if the SSID is available for registration, false if it already has an account registered. + */ checkSSIDAvailable(ssid) { var directory = (directory) ? directory : this.session_store_dir + this.path.sep + "accounts"; var available = true; @@ -41,6 +51,12 @@ class WTVRegister { return available; } + /** + * Checks if the username is already taken. + * @param {string} username The username to check + * @param {string} directory The directory to search for user accounts + * @returns {boolean} True if the username is available, false if it is already taken + */ checkUsernameAvailable(username, directory = null) { var self = this; var return_val = false; diff --git a/zefie_wtvp_minisrv/includes/classes/WTVSSL.js b/zefie_wtvp_minisrv/includes/classes/WTVSSL.js index ccbbff20..d3ea0906 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVSSL.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVSSL.js @@ -7,7 +7,7 @@ class WTVSSL { getCACert() { // return the CA cert - const caCertFile = this.wtvshared.getServiceDep("https/ca.der") + const caCertFile = this.wtvshared.getServiceDep("https/ca.der", true) if (!this.wtvshared.fs.existsSync(caCertFile)) { throw new Error("CA certificate file not found"); } diff --git a/zefie_wtvp_minisrv/includes/classes/WTVSec.js b/zefie_wtvp_minisrv/includes/classes/WTVSec.js index f32ce9a6..49ef766e 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVSec.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVSec.js @@ -2,6 +2,7 @@ const CryptoJS = require('crypto-js'); const endianness = require('endianness'); var RC4 = require('rc4-crypto'); var crypto = require('crypto'); +var WTVShared = require("./WTVShared.js")['WTVShared']; /** * Javascript implementation of WTVP Security @@ -34,6 +35,7 @@ class WTVSec { RC4Session = new Array(); minisrv_config = []; update_ticket = false; + wtvshared = null; ticket_store = {}; /** @@ -46,6 +48,7 @@ class WTVSec { */ constructor(minisrv_config, wtv_incarnation = 1) { this.minisrv_config = minisrv_config; + this.wtvshared = new WTVShared(minisrv_config); this.initial_shared_key = CryptoJS.enc.Base64.parse(this.minisrv_config.config.keys.initial_shared_key); if (this.initial_shared_key.sigBytes === 8) { @@ -58,7 +61,6 @@ class WTVSec { /** * Set the wtv-incarnation for this instance - * * @param {Number} wtv_incarnation */ set_incarnation(wtv_incarnation) { @@ -77,16 +79,16 @@ class WTVSec { /** * Clones a WordArray to allow modification without referencing its original - * @param {CryptoJS.lib.WordArray} wa - * + * @param {CryptoJS.lib.WordArray} wordArray * @returns {CryptoJS.lib.WordArray} */ - DuplicateWordArray(wa) { - return CryptoJS.lib.WordArray.create(this.wordArrayToBuffer(wa)); + DuplicateWordArray(wordArray) { + return CryptoJS.lib.WordArray.create(this.wordArrayToBuffer(wordArray)); } /** * Prepares the wtv-ticket for this instance + * @returns {Base64} wtv-ticket */ PrepareTicket() { // store last challenge response in ticket @@ -110,20 +112,8 @@ class WTVSec { return this.ticket_b64; } - tryDecodeJSON(json_string) { - var out; - try { - out = JSON.parse(json_string); - } catch (e) { - console.log(e); - out = {}; - } - return out; - } - /** * Decodes a wtv-ticket to set up this instance - * * @param {Base64} ticket_b64 */ DecodeTicket(ticket_b64) { @@ -146,7 +136,7 @@ class WTVSec { var challenge_code_b64 = CryptoJS.enc.Hex.parse(challenge_code).toString(CryptoJS.enc.Base64); if ((ticket_dec.sigBytes * 2) >= challenge_code.length) { var ticket_data_dec = CryptoJS.enc.Hex.parse(ticket_dec.toString().substring(data_offset)).toString(CryptoJS.enc.Utf8); - this.ticket_store = this.tryDecodeJSON(ticket_data_dec); + this.ticket_store = this.wtvshared.tryDecodeJSON(ticket_data_dec); } else { this.ticket_store = {}; } @@ -155,6 +145,11 @@ class WTVSec { if (this.minisrv_config.config.debug_flags.debug) console.log(" * Decoded session from wtv-ticket with ticket_store:", this.ticket_store); } + /** + * Gets the ticket data for this instance + * @param {string} key The key of the ticket data to retrieve + * @returns {any} The ticket data for the specified key, or null if not found + */ getTicketData(key = null) { if (typeof (this.ticket_store) === 'session_store') return null; else if (key === null) return this.ticket_store; @@ -162,6 +157,11 @@ class WTVSec { else return null; } + /** + * Sets the ticket data for this instance + * @param {string} key The key of the ticket data to set + * @param {any} value The value to set for the specified key + */ setTicketData(key, value) { if (key === null) throw ("WTVSec.setTicketData(): invalid key provided"); if (typeof (this.ticket_store) === 'undefined') this.ticket_store = {}; @@ -170,6 +170,10 @@ class WTVSec { this.update_ticket = true; } + /** + * Deletes the ticket data for this instance + * @param {string} key The key of the ticket data to delete + */ deleteTicketData(key) { if (key === null) throw ("WTVSec.deleteTicketData(): invalid key provided"); if (typeof (this.ticket_store) === 'undefined') { @@ -234,7 +238,6 @@ class WTVSec { /** * Generates a wtv-challenge for this instance - * * @returns {Base64} wtv-challenge */ IssueChallenge() { @@ -246,7 +249,7 @@ class WTVSec { * bytes 64 - 80: session key 2 used in RC4 encryption triggered by SECURE ON * bytes 80 - 88: new key for future challenges * bytes 88 - 104: MD5 of 8 - 88 - * bytes 104 - 112: padding.not important + * bytes 104 - 112: padding. seemingly not important, but by default is 8 bytes of 0x08 */ const challenge_id = CryptoJS.lib.WordArray.random(8); const echo_me = CryptoJS.lib.WordArray.random(40); @@ -280,8 +283,7 @@ class WTVSec { /** * convert a CryptoJS.lib.WordArray to a Javascript Buffer * @param {CryptoJS.lib.WordArray} wordArray - * - * #returns {Buffer} JS Buffer object + * @returns {Buffer} JS Buffer object */ wordArrayToBuffer(wordArray) { if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex'); @@ -291,7 +293,6 @@ class WTVSec { /** * Starts an encryption session * @param {Number} rc4session Session Type (0 = enc k1, 1 = dec k1, 2 = enc k2, 3 = dec k2, default: all) - * */ SecureOn(rc4session = null) { if (this.minisrv_config.config.debug_flags.debug) console.log(" # Generating RC4 sessions with wtv-incarnation: " + this.incarnation); @@ -326,7 +327,6 @@ class WTVSec { * RC4 Encrypt data * @param {Number} keynum Which key to use (0 = k1, 1 = k2) * @param {CryptoJS.lib.WordArray|ArrayBuffer|Buffer} data Data to encrypt - * * @returns {ArrayBuffer} Encrypted data */ Encrypt(keynum, data) { @@ -357,8 +357,8 @@ class WTVSec { * RC4 Decrypt data * @param {Number} keynum Which key to use (0 = k1, 1 = k2) * @param {CryptoJS.lib.WordArray|ArrayBuffer|Buffer} data Data to decrypt - * * @returns {ArrayBuffer} Decrypted data + * @notice This function is an alias for Encrypt, as WTVSec uses the same method for both encryption and decryption. */ Decrypt(keynum, data) { return this.Encrypt(keynum, data) diff --git a/zefie_wtvp_minisrv/includes/classes/WTVShared.js b/zefie_wtvp_minisrv/includes/classes/WTVShared.js index 1040afb9..563c4806 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVShared.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVShared.js @@ -23,6 +23,12 @@ class WTVShared { minisrv_config = []; + /** + * Constructor for WTVShared class + * @param {object} minisrv_config The configuration object for the minisrv + * @param {boolean} quiet If true, suppresses console output + * @notice If minisrv_config is null, it will attempt to read the configuration from the minisrv_config.json file + * */ constructor(minisrv_config, quiet = false) { if (minisrv_config == null) this.minisrv_config = this.readMiniSrvConfig(true, !quiet); else this.minisrv_config = minisrv_config; @@ -47,7 +53,120 @@ class WTVShared { } } + /** + * Converts an IP address to a hexadecimal string (WTV) + * @param {string} ip The IP address to convert + * @returns {string} The hexadecimal representation of the IP address + * @throws {Error} If the IP address is invalid + */ + ipToHex(ip) { + const parts = ip.split('.'); + if (parts.length !== 4) { + throw new Error('Invalid IP address'); + } + let num = 0; + for (let i = 0; i < 4; i++) { + const part = parseInt(parts[i], 10); + if (part < 0 || part > 255) { + throw new Error('Invalid IP address'); + } + num = (num << 8) | part; + } + // Convert to unsigned 32-bit number before converting to hex + return "0x" + (num >>> 0).toString(16).toUpperCase(); + } + /** + * Converts an IP address to a long integer + * @param {string} ip The IP address to convert + * @returns {number} The long integer representation of the IP address, or -1 if the IP address is invalid + */ + ip2long(ip) { + var components; + if (components = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) { + var iplong = 0; + var power = 1; + for (var i = 4; i >= 1; i -= 1) { + iplong += power * parseInt(components[i]); + power *= 256; + } + return iplong; + } + else return -1; + } + + /** + * Checks if an IP address is in a given subnet. + * @param {string} ip The IP address to check + * @param {string} subnet The subnet in CIDR notation + * @returns {boolean} True if the IP address is in the subnet, false otherwise + */ + isInSubnet(ip, subnet) { + if (subnet.indexOf('/') == -1) { + var mask, base_ip, long_ip = this.ip2long(ip); + var mask2, base_ip2, long_ip2 = this.ip2long(ip); + return (long_ip == long_ip2); + } else { + var mask, base_ip, long_ip = this.ip2long(ip); + if ((mask = subnet.match(/^(.*?)\/(\d{1,2})$/)) && ((base_ip = this.ip2long(mask[1])) >= 0)) { + var freedom = Math.pow(2, 32 - parseInt(mask[2])); + return (long_ip > base_ip) && (long_ip < base_ip + freedom - 1); + } + } + return false; + } + + /** + * Converts a byte array to a 32-bit unsigned integer (big-endian) + * @param {Uint8Array} bytes The byte array + * @param {number} offset The offset within the byte array + * @returns {number} The 32-bit unsigned integer + */ + toUint32(bytes, offset) { + return ( + (bytes[offset] << 24) >>> 0 | + (bytes[offset + 1] << 16) | + (bytes[offset + 2] << 8) | + (bytes[offset + 3]) + ) >>> 0; + } + + /** + * Converts a 32-bit unsigned integer to a byte array (big-endian) + * @param {number} num The 32-bit unsigned integer + * @returns {number[]} The byte array representation + * @notice The output is an array of 4 bytes, each byte is an unsigned integer (0-255) + */ + uint32ToBytes(num) { + return [ + (num >>> 24) & 0xff, + (num >>> 16) & 0xff, + (num >>> 8) & 0xff, + num & 0xff, + ]; + } + + /** + * Tries to decode a JSON string + * @param {String} json_string + * @returns {Object} The decoded JSON object, or an empty object if decoding fails. + */ + tryDecodeJSON(json_string) { + var out; + try { + out = JSON.parse(json_string); + } catch (e) { + console.log(e); + out = {}; + } + return out; + } + + /** + * Gets the box name based on the client ROM type + * @param {string} client_rom_type The client ROM type + * @returns {string} The box name + */ getBoxName(client_rom_type) { switch (client_rom_type) { case "bf0app": @@ -94,14 +213,22 @@ class WTVShared { return crc.toString(16).padStart(2, '0'); } - // check if the SSID has a valid checksum + /** + * Checks if the SSID has a valid checksum + * @param {string} ssid The SSID to check + * @return {boolean} true if the SSID is valid, false if not + */ checkSSID(ssid) { if (ssid.slice(-2) == this.getSSIDCRC(ssid)) return true; return false; } - + /** + * Parses variables in a string, replacing %ServiceDeps% with the service dependencies + * @param {string} s The string to parse + * @returns {string} The parsed string + */ parseConfigVars(s) { if (s.indexOf("%ServiceDeps%") >= 0) return this.getServiceDep(s.replace("%ServiceDeps%", ""), true); @@ -155,7 +282,6 @@ class WTVShared { return src; } - /** * Checks if the user has been whitelisted for wtv-admin * @param {object} wtvclient the clientSessionData object for the user @@ -169,6 +295,11 @@ class WTVShared { return result; } + /** + * Parses a JSON string, removing unsupported comments + * @param {string} json JSON string to parse + * @returns {object} Parsed JSON object + */ parseJSON(json) { if (typeof json !== 'string') json = json ? json.toString() : ''; let result = ''; @@ -206,7 +337,6 @@ class WTVShared { return JSON.parse(result); } - /** * Attempts to convert val into a boolean * @param {string,int,boolean} val @@ -258,7 +388,6 @@ class WTVShared { return entitized; } - /** * Attempts to sanitize HTML code to remove possible exploits when embedded in a WebTV Service * @param {string} string The string to sanitize @@ -332,7 +461,7 @@ class WTVShared { * @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 = {}; @@ -421,7 +550,6 @@ class WTVShared { return typeof str === 'string' && /^[\x00-\x7F]*$/.test(str); } - /** * Attempts to determine if the string contains HTML * @param {string} str @@ -432,7 +560,6 @@ class WTVShared { return typeof str === 'string' && pattern.test(str); } - /** * Attempts to determine if the string is Base64 or not * @param {string} str String to check @@ -455,7 +582,11 @@ class WTVShared { return new RegExp(regex, 'gi').test(str); } - + /** + * Decodes a UTF-8 string to a regular string + * @param {string} utf8String The UTF-8 encoded string to decode + * @returns {string} The decoded string + * */ utf8Decode(utf8String) { if (typeof utf8String !== 'string') { throw new TypeError("parameter 'utf8String' is not a string"); @@ -465,7 +596,11 @@ class WTVShared { return textDecoder.decode(bytes); } - + /** + * Decodes a buffer containing ISO-8859-1 encoded text to a UTF-8 string + * @param {Buffer} buf The buffer to decode + * @returns {string} The decoded string + */ decodeBufferText(buf) { var out = ""; out = this.utf8Decode(this.iconv.decode(Buffer.from(buf),'ISO-8859-1')); @@ -487,7 +622,6 @@ class WTVShared { return this.fixPathSlashes(check_path); } - /** * Detects if the client is in MiniBrowser mode * @param {object} ssid_session @@ -507,6 +641,12 @@ class WTVShared { return (this.isMiniBrowser(ssid_session) || parseInt(ssid_session.get("wtv-system-version")) < minBuild) ? true : false; } + /** + * Gets the user configuration from the user_config.json file + * @returns {object} User configuration object + * @notice If the file does not exist, it will return an empty object + * @notice If the file exists but cannot be parsed, it will terminate the process with an error message + */ getUserConfig() { try { var user_config_filename = this.getAbsolutePath("user_config.json", this.appdir); @@ -577,7 +717,6 @@ class WTVShared { return ssid_obj; } - /** * Alias for parseSSID, but just the manufacture info * @param {string} ssid @@ -589,6 +728,13 @@ class WTVShared { else return this.parseSSID(ssid).manufacturer || null; } + /** + * Reads the MiniSrv configuration files + * @param {boolean} user_config If true, also read user_config.json + * @param {boolean} notices If true, show notices + * @param {boolean} reload_notice If true, show reload notice + * @returns {object} The MiniSrv configuration object + */ readMiniSrvConfig(user_config = true, notices = true, reload_notice = false) { const log = (msg) => { if (notices || reload_notice) console.log(msg); @@ -642,8 +788,15 @@ class WTVShared { log(" *** Configuration successfully read."); this.minisrv_config = minisrv_config; return this.minisrv_config; -} + } + /** + * Integrates the user configuration into the main configuration object + * @param {object} main The main configuration object + * @param {object} user The user configuration object + * @returns {object} The integrated configuration object + * @notice This will overwrite any existing keys in the main configuration with the user configuration + * */ integrateConfig(main, user) { for (const key in user) { if (typeof user[key] === 'object' && user[key] !== null && !Array.isArray(user[key])) { @@ -655,7 +808,11 @@ class WTVShared { return main; } - + /** + * Writes the user configuration to the user_config.json file + * @param {object} config Configuration object to write + * @returns {boolean} true if successful, false if not + */ writeToUserConfig(config) { if (config) { try { @@ -684,7 +841,6 @@ class WTVShared { return false; } - /** * Generates a random string * @param {int} len desired generated string length @@ -718,7 +874,6 @@ class WTVShared { return result; } - /** * Any alias of generateString with optional special characters enabled as well * @param {string} len desired generated string length @@ -737,6 +892,13 @@ class WTVShared { return this.minisrv_config; } + /** + * Wraps a string to a specified length, breaking at whitespace + * @param {string} string The string to wrap + * @param {number} len The maximum line length + * @param {string} join The string to join the wrapped lines with (default is "\n") + * @returns {string} The wrapped string + */ lineWrap(string, len = 72, join = "\n") { if (string.length <= len) return string; @@ -771,6 +933,11 @@ class WTVShared { return this.getFileLastModified(file).toUTCString(); } + /** + * Returns the Last-Modified date in a Date object with UTC time + * @param {string} file Path to a file + * @return {Date} Date object with UTC time + */ getFileLastModifiedUTCObj(file) { return new Date(new Date().setUTCSeconds(this.getFileLastModified(file).getUTCSeconds())); } @@ -842,6 +1009,11 @@ class WTVShared { return decoded; } + /** + * Censors the SSID by replacing parts of it with asterisks + * @param {string} ssid The SSID to censor + * @returns {string} Censored SSID + * */ censorSSID(ssid) { if (ssid) { if (ssid.slice(0, 8) === "MSTVSIMU") { @@ -875,7 +1047,11 @@ class WTVShared { return obj; } - + /** + * Filters sensitive information from request logs + * @param {object} obj The request log object to filter + * @returns {object} Filtered request log object + * */ filterRequestLog(obj) { const passwordRegex = /(^pass$|passw(or)?d)/i; var newobj = this.cloneObj(obj); // Clone the object once at the beginning @@ -891,8 +1067,11 @@ class WTVShared { return newobj; } - - + /** + * Decodes post data from a request log object + * @param {object} obj The request log object + * @returns {object} The request log object with decoded post data + */ decodePostData(obj) { if (obj.post_data) { const filterPasswords = this.minisrv_config.config.filter_passwords_in_logs === true; @@ -924,11 +1103,12 @@ class WTVShared { 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) { + // Prevent usage + return; // Search for the module in the require cache let resolvedPath = require.resolve(moduleName); @@ -973,8 +1153,6 @@ class WTVShared { return this.path.resolve(path); } - - /** * Returns a percentage * @param {number} partialValue @@ -1005,6 +1183,12 @@ class WTVShared { return path.reverse().split(".")[0].reverse(); } + /** + * Gets a line from a file by line number + * @param {string} filename The file to read + * @param {number} lineNo The line number to read (0-indexed) + * @param {function} callback Callback function to call with the line or an error + * */ getLineFromFile(filename, lineNo, callback) { let lineCount = 0; const lineReader = this.readline.createInterface({ @@ -1034,8 +1218,6 @@ class WTVShared { }); } - - /** * Checks if service is enabled or disabled in the config * @param {string} service Service Name @@ -1127,7 +1309,6 @@ class WTVShared { return [headers, message]; } - /** * Strips bad things from paths * @param {string} base Base path @@ -1169,7 +1350,6 @@ class WTVShared { return pathModule.normalize(normalizedPath); } - /** * Makes sure an SSID is clean, and doesn't contain any exploitable characters * @param {string} ssid @@ -1181,12 +1361,22 @@ class WTVShared { return ssid; } + /** + * Makes sure a string path is clean, and doesn't contain any exploitable characters + * @param {string} path + * @returns {string|null} Sanitized Path + * */ makeSafeStringPath(path = "") { path = path.replace(/[^\w]/g, "").replace(/\.\./g, ""); if (path.length == 0) path = null; return path; } + /** + * Unpacks a base64 compressed string + * @param {string|Buffer} data Base64 encoded compressed data + * @returns {string} Uncompressed string + * */ unpackCompressedB64(data) { var data_buf = (typeof data === 'object') ? Buffer.from(data.toString('ascii'), 'base64') : Buffer.from(data, 'base64'); return this.zlib.inflateSync(data_buf, { finishFlush: this.zlib.Z_SYNC_FLUSH }).toString('ascii'); @@ -1252,6 +1442,7 @@ class WTVShared { } return keys.indexOf(key); } + /** * Moves an object to the desired location in the object (reorder) * @param {string|int} currentKey Name of the object Key to move or the index to move @@ -1322,7 +1513,7 @@ class clientShowAlert { * @param {string} buttonlabel2 Button 2 Label * @param {string} buttonaction2 Button 2 Action * @param {string} noback If true, disables the back button - * @param {string} sound Sound to play + * @param {string} sound Sound to play when showing the alert (default is "none") */ constructor(image = null, message = null, buttonlabel1 = null, buttonaction1 = null, buttonlabel2 = null, buttonaction2 = null, noback = null, sound = null) { this.message = message; diff --git a/zefie_wtvp_minisrv/includes/classes/WTVShenanigans.js b/zefie_wtvp_minisrv/includes/classes/WTVShenanigans.js index 99813039..a7148a7d 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVShenanigans.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVShenanigans.js @@ -9,14 +9,27 @@ class WTVShenanigans { "DISABLE_HTML_SANITIZER": 5 // disables HTML Sanitizer, allowing all sorts of chaos in email/usenet posts and signatures } + /** + * Creates an instance of WTVShenanigans. + * @param {Object} minisrv_config - The minisrv configuration object. + */ constructor(minisrv_config) { this.minisrv_config = minisrv_config; } + /** + * Returns the current shenanigans level set in the minisrv configuration. + * @returns {boolean|number} The shenanigans level, or false if shenanigans are disabled. + */ getShenanigansLevel() { return this.minisrv_config.config.shenanigans; } + /** + * Checks if a specific shenanigan is enabled based on the current shenanigans level. + * @param {number} value - The shenanigan level to check against. + * @returns {boolean} True if the shenanigan is enabled, false otherwise. + */ checkShenanigan(value) { var level = this.getShenanigansLevel(); diff --git a/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js b/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js index ecaff398..2d535ddf 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVTellyScript.js @@ -1,4 +1,5 @@ const LZSS = require("./LZSS.js"); +const WTVShared = require("./WTVShared.js")['WTVShared']; const WhitespaceInstruction = { ADD_NONE: 0, @@ -278,7 +279,7 @@ class WTVTellyScriptTokenizer { this.tokenizeIdentifierOrConstant(checkSequence); } } else { - // Not alphanumeric – try symbol sequence. + // Not alphanumeric � try symbol sequence. this.index = currentIdx; checkSequence = this.buildCheckSequence(ch, "^[\\-+=<>!\\|\\&]$"); if (this.replacements.has(checkSequence)) { @@ -1048,7 +1049,7 @@ class WTVTellyScriptMinifier { class WTVTellyScript { // --- TellyScript Class --- - /* + /** * Constructs a new TellyScript object. * @param {Uint8Array|string} data - The TellyScript data (either packed, tokenized, or raw). * @param {number} dataState - One of TellyScriptState (default: PACKED). @@ -1064,10 +1065,14 @@ class WTVTellyScript { this.raw_data = null; this.preprocessor_definitions = preprocessor_definitions; this.version_minor = version_minor; - + this.wtvshared = new WTVShared(); this.process(data, dataState); } + /** + * Preprocesses the tellscript data based on the current preprocessor definitions. + * It handles directives like #ifdef, #ifndef, #if, #else, #endif. + */ preprocess() { var definitions = this.preprocessor_definitions || {}; // Split input into lines (handling CRLF and LF) @@ -1140,6 +1145,10 @@ class WTVTellyScript { } + /** + * Minifies the TellyScript data. + * @returns {string} The minified TellyScript data. + */ minify() { let minifier = new WTVTellyScriptMinifier(); this.raw_data = minifier.minify(this); @@ -1148,51 +1157,18 @@ class WTVTellyScript { this.pack(); } - ipToHex(ip) { - const parts = ip.split('.'); - if (parts.length !== 4) { - throw new Error('Invalid IP address'); - } - let num = 0; - for (let i = 0; i < 4; i++) { - const part = parseInt(parts[i], 10); - if (part < 0 || part > 255) { - throw new Error('Invalid IP address'); - } - num = (num << 8) | part; - } - // Convert to unsigned 32-bit number before converting to hex - return "0x" + (num >>> 0).toString(16).toUpperCase(); - } - setTemplateVars(service_name, dialin_number, DNS1IP, DNS2IP) { this.raw_data = this.raw_data.replaceAll("%ServiceName%", service_name); this.raw_data = this.raw_data.replaceAll("%DialinNumber%", dialin_number); this.raw_data = this.raw_data.replaceAll("%DNSIP1%", DNS1IP); this.raw_data = this.raw_data.replaceAll("%DNSIP2%", DNS2IP); - this.raw_data = this.raw_data.replaceAll("%DNS1%", this.ipToHex(DNS1IP)); - this.raw_data = this.raw_data.replaceAll("%DNS2%", this.ipToHex(DNS2IP)); + this.raw_data = this.raw_data.replaceAll("%DNS1%", this.wtvshared.ipToHex(DNS1IP)); + this.raw_data = this.raw_data.replaceAll("%DNS2%", this.wtvshared.ipToHex(DNS2IP)); } // --- Big Endian Converter Helpers --- - toUint32(bytes, offset) { - return ( - (bytes[offset] << 24) >>> 0 | - (bytes[offset + 1] << 16) | - (bytes[offset + 2] << 8) | - (bytes[offset + 3]) - ) >>> 0; - } - uint32ToBytes(num) { - return [ - (num >>> 24) & 0xff, - (num >>> 16) & 0xff, - (num >>> 8) & 0xff, - num & 0xff, - ]; - } // --- CRC32 Calculation --- @@ -1253,7 +1229,7 @@ class WTVTellyScript { autoDetectState(data) { if (data instanceof Uint8Array) { if (data.length > 4) { - const magic = this.toUint32(data, 0); + const magic = this.wtvshared.toUint32(data, 0); if (magic === 0x414e4459) { // "ANDY" this.tellyscript_type = TellyScriptType.ORIGINAL; return TellyScriptState.PACKED; @@ -1312,19 +1288,23 @@ class WTVTellyScript { } // --- Unpacking --- + /** + * Unpacks the packed TellyScript data + * @returns {Uint8Array} The unpacked TellyScript data + */ unpack() { // Read header fields from the first 36 bytes. const headerBytes = this.packed_data.slice(0, PACKED_TELLYSCRIPT_HEADER_SIZE); this.packed_header = { magic: String.fromCharCode(...headerBytes.slice(0, 4)), - version_major: this.toUint32(headerBytes, 4), - version_minor: this.toUint32(headerBytes, 8), - script_id: this.toUint32(headerBytes, 12), - script_mod: this.toUint32(headerBytes, 16), - compressed_data_length: this.toUint32(headerBytes, 20), - decompressed_data_length: this.toUint32(headerBytes, 24), - decompressed_checksum: this.toUint32(headerBytes, 28), - unknown1: this.toUint32(headerBytes, 32), + version_major: this.wtvshared.toUint32(headerBytes, 4), + version_minor: this.wtvshared.toUint32(headerBytes, 8), + script_id: this.wtvshared.toUint32(headerBytes, 12), + script_mod: this.wtvshared.toUint32(headerBytes, 16), + compressed_data_length: this.wtvshared.toUint32(headerBytes, 20), + decompressed_data_length: this.wtvshared.toUint32(headerBytes, 24), + decompressed_checksum: this.wtvshared.toUint32(headerBytes, 28), + unknown1: this.wtvshared.toUint32(headerBytes, 32), }; // Extract compressed data from the remainder of the packed_data. @@ -1337,21 +1317,30 @@ class WTVTellyScript { return this.tokenized_data; } - // --- Detokenization --- + /** + * Detokenizes the tokenized TellyScript data + * @returns {string} The detokenized TellyScript data + */ detokenize() { // Uses the previously defined TellyScriptDetokenizer class. this.raw_data = new WTVTellyScriptDetokenizer(this.tokenized_data).detokenize(); return this.raw_data; } - // --- Tokenization --- + /** + * Tokenizes the raw TellyScript data + * @returns {Uint8Array} The tokenized TellyScript data + */ tokenize() { // Uses the previously defined TellyScriptTokenizer class. this.tokenized_data = new WTVTellyScriptTokenizer(this.raw_data).tokenize(); return this.tokenized_data; } - // --- Packing --- + /** + * Packs the tokenized TellyScript data into a packed format + * @returns {Uint8Array} The packed TellyScript data + */ pack() { // Compress tokenized data using LZSS. const comp = new LZSS(); @@ -1399,14 +1388,14 @@ class WTVTellyScript { buffer[i] = header.magic.charCodeAt(i); } // Next fields: each 4 bytes in Big Endian order. - buffer.set(this.uint32ToBytes(header.version_major), 4); - buffer.set(this.uint32ToBytes(header.version_minor), 8); - buffer.set(this.uint32ToBytes(header.script_id), 12); - buffer.set(this.uint32ToBytes(header.script_mod), 16); - buffer.set(this.uint32ToBytes(header.compressed_data_length), 20); - buffer.set(this.uint32ToBytes(header.decompressed_data_length), 24); - buffer.set(this.uint32ToBytes(header.decompressed_checksum), 28); - buffer.set(this.uint32ToBytes(header.unknown1), 32); + buffer.set(this.wtvshared.uint32ToBytes(header.version_major), 4); + buffer.set(this.wtvshared.uint32ToBytes(header.version_minor), 8); + buffer.set(this.wtvshared.uint32ToBytes(header.script_id), 12); + buffer.set(this.wtvshared.uint32ToBytes(header.script_mod), 16); + buffer.set(this.wtvshared.uint32ToBytes(header.compressed_data_length), 20); + buffer.set(this.wtvshared.uint32ToBytes(header.decompressed_data_length), 24); + buffer.set(this.wtvshared.uint32ToBytes(header.decompressed_checksum), 28); + buffer.set(this.wtvshared.uint32ToBytes(header.unknown1), 32); return buffer; } diff --git a/zefie_wtvp_minisrv/includes/config.json b/zefie_wtvp_minisrv/includes/config.json index 88c5c73b..f101a060 100644 --- a/zefie_wtvp_minisrv/includes/config.json +++ b/zefie_wtvp_minisrv/includes/config.json @@ -58,7 +58,9 @@ "reserved_names_files": [ "includes/badWords.json", "includes/reservedWords.json" - ] + ], + "filestore_storage": 16, // Megabytes + "scrapbook_storage": 8 // Megabytes }, "irc": { "enabled": false,