diff --git a/.gitignore b/.gitignore index 25a2816b..527f3261 100644 --- a/.gitignore +++ b/.gitignore @@ -367,3 +367,4 @@ FodyWeavers.xsd /zefie_wtvp_minisrv/UserServiceVault/*-*/ /zefie_wtvp_minisrv/ServiceLogPost/*.log /zefie_wtvp_minisrv/SessionStore/*.json +/zefie_wtvp_minisrv/SessionStore/*/ diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/noflash.js b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/noflash.js index 984e5442..1c68e5b8 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/noflash.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/noflash.js @@ -1,5 +1,5 @@ if (socket.ssid != null && !ssid_sessions[socket.ssid].get("wtvsec_login")) { - var wtvsec_login = new WTVSec(); + var wtvsec_login = new WTVSec(minisrv_config); wtvsec_login.IssueChallenge(); wtvsec_login.set_incarnation(request_headers["wtv-incarnation"]); ssid_sessions[socket.ssid].set("wtvsec_login", wtvsec_login); diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js index fd2bfc70..56180d57 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js @@ -2,7 +2,7 @@ if (socket.ssid) { if (ssid_sessions[socket.ssid].loadSessionData() == true) { - console.log(" * Loaded session data from disk for", filterSSID(socket.ssid)) + console.log(" * Loaded session data from disk for", wtvshared.filterSSID(socket.ssid)) ssid_sessions[socket.ssid].setSessionData("registered", (ssid_sessions[socket.ssid].getSessionData("registered") == true) ? true : false); } else { ssid_sessions[socket.ssid].session_data = {}; @@ -20,15 +20,15 @@ } } }); - if (i > 0 && zdebug) console.log(" # Closed", i, "previous sockets for", filterSSID(socket.ssid)); + if (i > 0 && minisrv_config.config.debug_flags.debug) console.log(" # Closed", i, "previous sockets for", wtvshared.filterSSID(socket.ssid)); } } if (ssid_sessions[socket.ssid].data_store.wtvsec_login) { - if (zdebug) console.log(" # Recreating primary WTVSec login instance for", filterSSID(socket.ssid)); + if (minisrv_config.config.debug_flags.debug) console.log(" # Recreating primary WTVSec login instance for", wtvshared.filterSSID(socket.ssid)); delete ssid_sessions[socket.ssid].data_store.wtvsec_login; } - ssid_sessions[socket.ssid].data_store.wtvsec_login = new WTVSec(); + ssid_sessions[socket.ssid].data_store.wtvsec_login = new WTVSec(minisrv_config); ssid_sessions[socket.ssid].data_store.wtvsec_login.IssueChallenge(); ssid_sessions[socket.ssid].data_store.wtvsec_login.set_incarnation(request_headers["wtv-incarnation"] || 1); } else { @@ -115,24 +115,23 @@ if (ssid_sessions[socket.ssid].data_store.wtvsec_login) { gourl = "wtv-head-waiter:/login-stage-two?relogin=true"; } - if (request_headers.query.reconnect) { - gourl = null; - } - - if (!file_path != null && !zquiet) console.log(" * Sending TellyScript", file_path, "on socket", socket.id); + if (request_headers.query.reconnect) gourl = null; if (request_headers.query.guest_login) { send_tellyscript = false; - gourl += "&guest_login=true" + if (gourl != null) gourl += "&guest_login=true" if (request_headers.query.skip_splash) gourl += "&skip_splash=true"; } + if (!file_path != null && send_tellyscript && !minisrv_config.config.debug_flags.quiet) console.log(" * Sending TellyScript", file_path, "on socket", socket.id); + + headers = "200 OK\n" if (bf0app_update) headers += "minisrv-use-carriage-return: false\n"; headers += "Connection: Keep-Alive\n"; headers += "wtv-initial-key: " + ssid_sessions[socket.ssid].data_store.wtvsec_login.challenge_key.toString(CryptoJS.enc.Base64) + "\n"; headers += "Content-Type: " + prereg_contype + "\n"; - headers += "wtv-service: reset\n"; + if (!request_headers.query.reconnect) headers += "wtv-service: reset\n"; if (!bf0app_update) headers += getServiceString('wtv-1800') + "\n"; if (bf0app_update) headers += getServiceString('wtv-head-waiter', { "flags": "0x00000001" }) + "\n"; diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/en-US/kflex/modem_firmware.dat.gz b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/en-US/kflex/modem_firmware.dat.gz new file mode 100644 index 00000000..8d709962 Binary files /dev/null and b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/en-US/kflex/modem_firmware.dat.gz differ diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/en-US/v90/modem_firmware.dat.gz b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/en-US/v90/modem_firmware.dat.gz new file mode 100644 index 00000000..f1df9b19 Binary files /dev/null and b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/en-US/v90/modem_firmware.dat.gz differ diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz new file mode 100644 index 00000000..45e80dc8 Binary files /dev/null and b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz differ diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/diskmaps/ModemFirmware.json b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/diskmaps/ModemFirmware.json new file mode 100644 index 00000000..93aebcff --- /dev/null +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/diskmaps/ModemFirmware.json @@ -0,0 +1,18 @@ +{ + "ModemFirmware": { + "base": "file://Disk/Browser/Modem_Firmware/", + "location": "content/Modem_Firmware/", + "execute": "client:ModemReload", + "execute_when": "atEnd", + "service_owned": true, + "files": [ + { + "file": "file://Disk/Browser/Modem_Firmware/Locale/en-US/modem_firmware.dat.gz", + "location": "content/Modem_Firmware/Locale/en-US/v90/modem_firmware.dat.gz" + }, + { + "file": "file://Disk/Browser/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz" + } + ] + } +} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/diskmaps/ModemFirmwareOld.json b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/diskmaps/ModemFirmwareOld.json new file mode 100644 index 00000000..ff770dfd --- /dev/null +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/content/diskmaps/ModemFirmwareOld.json @@ -0,0 +1,18 @@ +{ + "ModemFirmware": { + "base": "file://Disk/Browser/Modem_Firmware/", + "location": "content/Modem_Firmware/", + "execute": "client:ModemReload", + "execute_when": "atEnd", + "service_owned": true, + "files": [ + { + "file": "file://Disk/Browser/Modem_Firmware/Locale/en-US/modem_firmware.dat.gz", + "location": "content/Modem_Firmware/Locale/en-US/kflex/modem_firmware.dat.gz" + }, + { + "file": "file://Disk/Browser/Modem_Firmware/Locale/ja-JP/modem_firmware.dat.gz" + } + ] + } +} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js index 8f8429b6..6f37edd8 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js @@ -1,88 +1,104 @@ -// todo: async +const WTVDownloadList = require("./WTVDownloadList.js"); +var wtvdl = new WTVDownloadList(minisrv_config, service_name); var force_update = (request_headers.query.force == "true") ? true : false; -console.log(force_update); if (request_headers['wtv-request-type'] == 'download') { - var path = require("path"); - var content_dir = "content/" var diskmap_dir = content_dir + "diskmaps/"; function generateDownloadList(diskmap_group_data, update_list, diskmap_data) { - // create WebTV Download List - var newest_file_epoch = 0; - var download_list = ''; - - if (diskmap_data.execute && diskmap_data.execute_when == "atStart") { - download_list += "EXECUTE " + diskmap_data.execute + "\n\n"; - } - - if (diskmap_data.partition_size) { - download_list += "CREATE " + diskmap_data.base + "\n"; - download_list += "partition-size: " + diskmap_data.partition_size + "\n\n"; - } - - download_list += "CREATE-GROUP " + diskmap_group_data + "-UPDATE\n"; - download_list += "state: invalid\n"; - download_list += "base: " + diskmap_data.base + ".GROUP-UPDATE/\n\n"; - - download_list += "CREATE-GROUP " + diskmap_group_data + "\n"; - download_list += "state: invalid\n"; - download_list += "service-owned: " + (diskmap_data.service_owned || false) + "\n"; - download_list += "base: " + diskmap_data.base + "\n\n"; - - Object.keys(update_list).forEach(function (k) { - if (!update_list[k].invalid) return; - download_list += "DELETE " + update_list[k].file.replace(diskmap_data.base, "") + "\n"; - download_list += "group: " + diskmap_group_data + "\n\n"; - }); - + wtvdl.reset(); + var files_to_send = 0; Object.keys(update_list).forEach(function (k) { if (update_list[k].checksum_match && !force_update) return; if (!update_list[k].invalid && !force_update) return; - download_list += "DISPLAY " + update_list[k].display + "\n\n"; - download_list += "GET " + update_list[k].file.replace(diskmap_data.base, "") + "\n"; - download_list += "group: " + diskmap_group_data + "-UPDATE\n"; - download_list += "location: " + service_name + ":/" + update_list[k].location + "\n"; - download_list += "file-permission: r\n" - download_list += "wtv-checksum: " + update_list[k].checksum + "\n"; - download_list += "service-source-location: /webtv/content/" + service_name.replace("wtv-", "") + "d/" + update_list[k].location + "\n"; - download_list += "client-dest-location: " + update_list[k].file + "\n\n"; + files_to_send++; }); - download_list += "CREATE-GROUP " + diskmap_group_data + "\n"; - download_list += "state: invalid\n"; - download_list += "service-owned: " + (diskmap_data.service_owned || false) + "\n"; - download_list += "base: " + diskmap_data.base + "\n\n"; - - - Object.keys(update_list).forEach(function (k) { - if (!update_list[k].invalid) return; - download_list += "RENAME " + update_list[k].file.replace(diskmap_data.base, "") + "\n"; - download_list += "group: " + diskmap_group_data + "-UPDATE\n"; - download_list += "destination-group: " + diskmap_group_data + "\n"; - download_list += "location: " + update_list[k].file.replace(diskmap_data.base, "") + "\n\n"; - }); - - download_list += "SET-GROUP " + diskmap_group_data + "\n"; - download_list += "state: ok\n"; - download_list += "version: " + diskmap_data.version + "\n"; - download_list += "last-checkup-time: " + new Date().toUTCString().replace("GMT", "+0000") + "\n\n"; - - if (diskmap_data.execute && diskmap_data.execute_when == "atEnd") { - download_list += "EXECUTE " + diskmap_data.execute + "\n\n"; + // create WebTV Download List + if (diskmap_data.execute && diskmap_data.execute_when) { + if (diskmap_data.execute_when.toLowerCase().match(/start/)) { + wtvdl.execute(diskmap_data.execute); + } } - download_list += "DELETE-GROUP " + diskmap_group_data + "-UPDATE\n\n"; - download_list += "DELETE " + diskmap_data.base + ".GROUP-UPDATE/\n\n"; - console.log(download_list); + if (diskmap_group_data.display) wtvdl.display(diskmap_group_data.display); + + if (files_to_send > 0) { + + if (diskmap_data.partition_size) { + wtvdl.createPartition(diskmap_data.base, diskmap_data.partition_size); + } + + if (!diskmap_data.nogroup) { + // only send group commands if group mode is enable + // useful to disable for PUT + wtvdl.createUpdateGroup(diskmap_group_data, diskmap_data.base, "invalid", (diskmap_data.service_owned || false)); + } + + Object.keys(update_list).forEach(function (k) { + // file { "action": "delete" } + // Useful to purge files we no longer want on the client + if (update_list[k].action != "DELETE") { + // skip deleting valid files if we aren't specifically requesting their deletion + if (update_list[k].checksum_match && !force_update) return; + if (!update_list[k].invalid && !force_update) return; + } + wtvdl.delete(update_list[k].file.replace(diskmap_data.base, ""), diskmap_group_data); + }); + + Object.keys(update_list).forEach(function (k) { + if (update_list[k].checksum_match && !force_update) return; + if (!update_list[k].invalid && !force_update) return; + if (update_list[k].display) wtvdl.display(update_list[k].display); + switch (update_list[k].action) { + case "PUT": + wtvdl.put(update_list[k].file.replace(diskmap_data.base, ""), service_name + ":/" + update_list[k].location, update_list[k].display); + break; + + case "GET": + wtvdl.get(update_list[k].file.replace(diskmap_data.base, ""), update_list[k].file, service_name + ":/" + update_list[k].location, diskmap_group_data, update_list[k].checksum, update_list[k].uncompressed_size || null, update_list[k].original_filename) + break; + } + }); + + if (!diskmap_data.nogroup) { + wtvdl.createGroup(diskmap_group_data, diskmap_data.base, "invalid", (diskmap_data.service_owned || false)); + + + // this rename loop is a part of the group system + Object.keys(update_list).forEach(function (k) { + if (update_list[k].checksum_match && !force_update) return; + if (!update_list[k].invalid && !force_update) return; + wtvdl.rename(update_list[k].file.replace(diskmap_data.base, ""), update_list[k].file.replace(diskmap_data.base, ""), diskmap_group_data, diskmap_group_data); + }); + + wtvdl.setGroup(diskmap_group_data, 'ok', diskmap_data.version); + } + + } + + if (diskmap_data.execute && diskmap_data.execute_when) { + if (diskmap_data.execute_when.toLowerCase().match(/end/)) { + wtvdl.execute(diskmap_data.execute); + } + } + + if (files_to_send > 0) { + if (!diskmap_data.nogroup) { + wtvdl.deleteGroupUpdate(diskmap_group_data, diskmap_data.base); + } + } + var download_list = wtvdl.getDownloadList(); + if (minisrv_config.config.show_diskmap) console.log(download_list); return download_list; } function processGroup(diskmap_primary_group, diskmap_group_data, diskmap_subgroup = null) { // parse webtv post var output_data = ''; - var post_data = request_headers.post_data.toString(CryptoJS.enc.Latin1).split("\n"); + var post_data = new Array(); + if (request_headers.post_data) post_data = request_headers.post_data.toString(CryptoJS.enc.Latin1).split("\n"); var post_data_current_directory = ''; var post_data_current_file = false; var post_data_current_group = ''; @@ -172,6 +188,8 @@ if (request_headers['wtv-request-type'] == 'download') { if (!fs.existsSync(post_match_file)) post_match_file = null; }); + + var post_match_file_lstat = fs.lstatSync(post_match_file); var post_match_file_data = new Buffer.from(fs.readFileSync(post_match_file, { encoding: null, @@ -180,10 +198,23 @@ if (request_headers['wtv-request-type'] == 'download') { diskmap_group_data.files[k].base = diskmap_group_data.base; diskmap_group_data.files[k].last_modified = (new Date(new Date(post_match_file_lstat.mtime).toUTCString()) / 1000); diskmap_group_data.files[k].content_length = post_match_file_lstat.size; - diskmap_group_data.files[k].checksum = CryptoJS.MD5(CryptoJS.lib.WordArray.create(post_match_file_data)).toString(CryptoJS.enc.Hex).toLowerCase(); + diskmap_group_data.files[k].action = (diskmap_group_data.files[k].action) ? diskmap_group_data.files[k].action.toUpperCase() : "GET"; + + if (wtvshared.getFileExt(post_match_file).toLowerCase() == "gz") { + // we need the checksum of the uncompressed data + var gunzipped = zlib.gunzipSync(post_match_file_data); + diskmap_group_data.files[k].checksum = CryptoJS.MD5(CryptoJS.lib.WordArray.create(gunzipped)).toString(CryptoJS.enc.Hex).toLowerCase(); + var gzip_fn_end = post_match_file_data.indexOf("\0", 10); + if (!diskmap_group_data.files[k].dont_extract_filename) { + diskmap_group_data.files[k].original_filename = post_match_file_data.toString('utf8', 10, gzip_fn_end); + } + //diskmap_group_data.files[k].uncompressed_size = gunzipped.byteLength; + gunzipped = null; + } else { + diskmap_group_data.files[k].checksum = CryptoJS.MD5(CryptoJS.lib.WordArray.create(post_match_file_data)).toString(CryptoJS.enc.Hex).toLowerCase(); + } if (parseInt(diskmap_group_data.files[k].last_modified) > newest_file_epoch) newest_file_epoch = parseInt(diskmap_group_data.files[k].last_modified); - if (!diskmap_group_data.files[k].display) diskmap_group_data.files[k].display = diskmap_group_data.display; diskmap_group_data.files[k].invalid = true; wtv_download_list.push(diskmap_group_data.files[k]); @@ -205,7 +236,7 @@ if (request_headers['wtv-request-type'] == 'download') { return output_data; } - if (request_headers.query.diskmap && request_headers.query.group && request_headers.post_data) { + if (request_headers.query.diskmap && request_headers.query.group) { var diskmap_json_file = null; Object.keys(service_vaults).forEach(function (g) { if (diskmap_json_file != null) return; @@ -243,69 +274,17 @@ if (request_headers['wtv-request-type'] == 'download') { var errpage = doErrorPage(404, "The requested DiskMap does not exist."); headers = errpage[0]; data = errpage[1]; - if (zdebug) console.error(" # " + service_name +":/sync error", "could not find diskmap"); + if (minisrv_config.config.debug_flags.debug) console.error(" # " + service_name +":/sync error", "could not find diskmap"); } } else { var errpage = doErrorPage(400); headers = errpage[0]; data = errpage[1]; - if (zdebug) console.error(" # " + service_name + ":/sync error", "missing query arguments"); + if (minisrv_config.config.debug_flags.debug) console.error(" # " + service_name + ":/sync error", "missing query arguments"); } } else if (request_headers.query.group && request_headers.query.diskmap) { var message = request_headers.query.message || "Retrieving files..."; var main_message = request_headers.query.main_message || "Your receiver is downloading files."; - headers = `200 OK -Content-Type: text/html`; - - data = ` - - - - - Retrieving files... - - - - - - - -
- - - - - - -
- ${message} -
-
-
-
- - - -
- - - ${main_message} -

This may take a while. - -

- -

- - - -
- - -`; - + headers = "200 OK\nwtv-connection-close: close\nConnection: close\nContent-Type: text/html"; + data = wtvdl.getSyncPage(message, request_headers.query.group, request_headers.query.diskmap, main_message, message, force_update) } \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/userstore.js b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/userstore.js new file mode 100644 index 00000000..f53bd496 --- /dev/null +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/userstore.js @@ -0,0 +1,21 @@ +if (request_headers.post_data) { + if (request_headers.query.partialPath || request_headers.query.path) { + if (socket.ssid) { + if (ssid_sessions[socket.ssid]) { + if (ssid_sessions[socket.ssid].isRegistered()) { + var result = ssid_sessions[socket.ssid].storeUserStoreFile(request_headers.query.path || request_headers.query.partialPath, new Buffer.from(request_headers.post_data.toString(CryptoJS.enc.Hex), 'hex'), request_headers.query['last-modified-seconds'] || null, (request_headers.query.no_overwrite) ? false : true); + if (result) { + headers = "200 OK\n"; + headers += "Content-Type: text/plain"; + } + } + } + } + } +} + +if (!headers) { + var errpage = doErrorPage(400) + headers = errpage[0]; + data = errpage[1]; +} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/content/content-serve.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/content/content-serve.js index b8209c2a..95fdea50 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/content/content-serve.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/content/content-serve.js @@ -1,6 +1,5 @@ const WTVFlashrom = require("./WTVFlashrom.js"); request_is_async = true; -console.log(request_headers); var bf0app_update = false; var request_path = request_headers.request_url.replace(service_name + ":/", ""); @@ -15,7 +14,7 @@ if ((romtype == "bf0app" || !romtype) && (bootver == "105" || !bootver)) { } if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) { - ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug); + ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update); } ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) { diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/current-noflash.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/current-noflash.js index 642803cf..25858295 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/current-noflash.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/current-noflash.js @@ -22,7 +22,7 @@ if (ssid_sessions[socket.ssid].get("wtv-client-rom-type") == "bf0app" && ssid_se } if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) { - ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug); + ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update); } ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) { diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-by-path.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-by-path.js index a2ad57e0..317c5fa4 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-by-path.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-by-path.js @@ -15,7 +15,7 @@ if ((romtype == "bf0app" || !romtype) && (bootver == "105" || !bootver)) { if (request_headers.query.raw || bf0app_update) { if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) { - ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug); + ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, bf0app_update); } ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) { diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-lc2-page.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-lc2-page.js index a7f93034..b71cfd69 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-lc2-page.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/get-lc2-page.js @@ -8,7 +8,7 @@ if (!request_headers.query.path) { headers = errpage[0]; data = errpage[1]; } else { - var wtvflashrom = new WTVFlashrom(service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, minisrv_config.services[service_name].debug); + var wtvflashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, minisrv_config.services[service_name].use_zefie_server, false, true); var request_path = request_headers.query.path; // read flashrom header info into array using WTVFlashrom class diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/initiate-lc2-download.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/initiate-lc2-download.js index 3a00509a..dc6ca784 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/initiate-lc2-download.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/initiate-lc2-download.js @@ -5,8 +5,9 @@ var romtype = ssid_sessions[socket.ssid].get("wtv-client-rom-type"); url = "client:updateflash?ipaddr=" + minisrv_config.services[service_name].host + "&port=" + minisrv_config.services[service_name].port + "&path=" + escape(service_name + ":/" +request_headers.query.path); if (request_headers.query.numparts) url += escape("&numparts=" + request_headers.query.numparts); } - headers = "200 OK\n"; + headers = "300 OK\n"; headers += "wtv-visit: " + url + "\n"; + headers += "Location: " + url + "\n"; headers += "Content-type: text/html"; data = ''; } else { diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/noflash.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/noflash.js index 03c990e9..1ee36f57 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/noflash.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/noflash.js @@ -22,7 +22,7 @@ if (ssid_sessions[socket.ssid].get("wtv-client-rom-type") == "bf0app" && ssid_se } if (!ssid_sessions[socket.ssid].data_store.WTVFlashrom) { - ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(service_vaults, service_name, 0, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug); + ssid_sessions[socket.ssid].data_store.WTVFlashrom = new WTVFlashrom(minisrv_config, service_vaults, service_name, 0, minisrv_config.services[service_name].use_zefie_server, bf0app_update, minisrv_config.services[service_name].debug); } ssid_sessions[socket.ssid].data_store.WTVFlashrom.getFlashRom(request_path, function (data, headers) { diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/willie.js b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/willie.js index 765de4e3..2ac678cf 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/willie.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-flashrom/willie.js @@ -32,7 +32,7 @@ const req = https.request(options, function (res) { }); res.on('error', function (e) { - if (!zquiet) console.log(" * Upstream Ultra Willies HTTP Error:", e); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Upstream Ultra Willies HTTP Error:", e); var errpage = doErrorPage(400) headers = errpage[0]; data = errpage[1]; @@ -40,7 +40,7 @@ const req = https.request(options, function (res) { }); res.on('end', function () { - if (!zquiet) console.log(" * Upstream Ultra Willies HTTP Response:", res.statusCode, res.statusMessage); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Upstream Ultra Willies HTTP Response:", res.statusCode, res.statusMessage); if (request_headers.query.clear_cache) { headers += "\nwtv-expire-all: "+service_name; } diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login-stage-two.js b/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login-stage-two.js index fde0566b..55efe6c4 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login-stage-two.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login-stage-two.js @@ -2,7 +2,7 @@ var challenge_response, challenge_header = ''; var gourl; if (socket.ssid != null && !ssid_sessions[socket.ssid].get("wtvsec_login")) { - var wtvsec_login = new WTVSec(1,zdebug); + var wtvsec_login = new WTVSec(minisrv_config); wtvsec_login.IssueChallenge(); wtvsec_login.set_incarnation(request_headers["wtv-incarnation"]); ssid_sessions[socket.ssid].set("wtvsec_login", wtvsec_login); @@ -16,13 +16,13 @@ if (socket.ssid !== null) { var client_challenge_response = request_headers["wtv-challenge-response"] || null; if (challenge_response && client_challenge_response) { if (challenge_response.toString(CryptoJS.enc.Base64) == client_challenge_response) { - console.log(" * wtv-challenge-response success for " + filterSSID(socket.ssid)); + console.log(" * wtv-challenge-response success for " + wtvshared.filterSSID(socket.ssid)); wtvsec_login.PrepareTicket(); } else { - console.log(" * wtv-challenge-response FAILED for " + filterSSID(socket.ssid)); - if (zdebug) console.log("Response Expected:", challenge_response.toString(CryptoJS.enc.Base64)); - if (zdebug) console.log("Response Received:", client_challenge_response) + console.log(" * wtv-challenge-response FAILED for " + wtvshared.filterSSID(socket.ssid)); + if (minisrv_config.config.debug_flags.debug) console.log("Response Expected:", challenge_response.toString(CryptoJS.enc.Base64)); + if (minisrv_config.config.debug_flags.debug) console.log("Response Received:", client_challenge_response) gourl = "wtv-head-waiter:/login?reissue_challenge=true"; } } else { @@ -147,6 +147,7 @@ wtv-open-isp-disabled: false wtv-offline-mail-enable: false wtv-demo-mode: 0 wtv-wink-deferrer-retries: 3 +wtv-name-server: 8.8.8.8 wtv-visit: ${home_url} Content-Type: text/html`; } \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login.js b/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login.js index 9f44b498..38e60ef4 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login.js @@ -54,7 +54,7 @@ wtv-log-url: wtv-log:/log`; if (challenge_header != "") headers += "\n" + challenge_header; headers += ` wtv-relogin-url: wtv-head-waiter:/relogin?relogin=true -wtv-reconnect-url: wwtv-head-waiter:/relogin?reconnect=true +wtv-reconnect-url: wtv-head-waiter:/relogin?reconnect=true wtv-visit: ${gourl} Content-type: text/html`; data = ''; diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js b/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js index 56e1609a..6b804319 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js @@ -5,14 +5,9 @@ wtv-expire-all: wtv-flashrom: Content-type: text/html` if (request_headers.query.url) headers += "\nwtv-visit: " + request_headers.query.url; +var cryptstatus = ((socket_sessions[socket.id].secure === true) ? "Encrypted" : "Not Encrypted") -if (ssid_sessions[socket.ssid].get('box-does-psuedo-encryption')) { - var cryptstatus = "Psuedo-encrypted"; -} else { - var cryptstatus = ((socket_sessions[socket.id].secure === true) ? "Encrypted" : "Not Encrypted") -} - -var comp_type = shouldWeCompress(socket.ssid,'text/html'); +var comp_type = wtvmime.shouldWeCompress(ssid_sessions[socket.ssid],'text/html'); var compstatus = "uncompressed"; switch (comp_type) { case 1: @@ -58,7 +53,7 @@ if (ssid_sessions[socket.ssid].hasCap("client-has-disk")) { data += "
  • DiskHax ~ VFatHax
  • \n"; if (ssid_sessions[socket.ssid].hasCap("client-can-do-macromedia-flash2")) { // only show demo if client can do flash2 - data += "
  • Old MSNTV DealerDemo: Download ~ Access (after Download)
  • \n"; + data += "
  • Old DealerDemo: Download ~ Access
  • \n"; } } diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-log/log.js b/zefie_wtvp_minisrv/ServiceVault/wtv-log/log.js index 47fa7299..b3e69ae5 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-log/log.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-log/log.js @@ -22,7 +22,7 @@ Content-length: 0`; logdata_outstring_hex += request_headers.post_data.toString(CryptoJS.enc.Hex); if (minisrv_config.services[service_name].write_logs_to_disk) { fs.writeFile(fullpath, logdata_outstring_hex, "Hex", function () { - if (!zquiet) console.log(" * Wrote POST log data from", filterSSID(socket.ssid), "for", socket.id); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Wrote POST log data from", wtvshared.filterSSID(socket.ssid), "for", socket.id); sendToClient(socket, headers, data); }); } else { @@ -41,7 +41,7 @@ Content-length: 0`; var logdata_outstring_hex = Buffer.from(logdata_outstring, 'utf8').toString('hex'); if (minisrv_config.services[service_name].write_logs_to_disk) { fs.writeFile(fullpath, logdata_outstring_hex, "Hex", function () { - if (!zquiet) console.log(" * Wrote GET log data from", filterSSID(socket.ssid), "for", socket.id); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Wrote GET log data from", wtvshared.filterSSID(socket.ssid), "for", socket.id); sendToClient(socket, headers, data); }); } else { diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-register/ValidateReviewAccountInfo.js b/zefie_wtvp_minisrv/ServiceVault/wtv-register/ValidateReviewAccountInfo.js index 32315cbf..9479296b 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-register/ValidateReviewAccountInfo.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-register/ValidateReviewAccountInfo.js @@ -17,7 +17,7 @@ if (!request_headers.query.registering || ssid_sessions[socket.ssid].setSessionData("subscriber_contact_method", request_headers.query.subscriber_contact_method); ssid_sessions[socket.ssid].setSessionData("subscriber_userid", '1' + Math.floor(Math.random() * 1000000000000000000)); ssid_sessions[socket.ssid].setSessionData("registered", true); - if (!ssid_sessions[socket.ssid].storeSessionData()) { + if (!ssid_sessions[socket.ssid].storeSessionData(true)) { var errpage = doErrorPage(400); headers = errpage[0]; data = errpage[1]; diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js index 06550461..bc3d6111 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js @@ -18,7 +18,7 @@ Content-Type: text/html` var wtv_system_version = ssid_sessions[socket.ssid].get("wtv-system-version"); var wtv_client_bootrom_version = ssid_sessions[socket.ssid].get("wtv-client-bootrom-version"); - var wtv_client_serial_number = filterSSID(ssid_sessions[socket.ssid].get("wtv-client-serial-number")); + var wtv_client_serial_number = wtvshared.filterSSID(ssid_sessions[socket.ssid].get("wtv-client-serial-number")); var wtv_client_rom_type = ssid_sessions[socket.ssid].get("wtv-client-rom-type"); var wtv_system_chipversion_str = ssid_sessions[socket.ssid].get("wtv-system-chipversion"); var wtv_system_sysconfig_hex = parseInt(ssid_sessions[socket.ssid].get("wtv-system-sysconfig")).toString(16); diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/unregister.js b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/unregister.js index 7f40113f..1f0af15a 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/unregister.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/unregister.js @@ -2,7 +2,9 @@ headers = `200 OK Content-Type: text/html`; if (!ssid_sessions[socket.ssid].getSessionData("registered")) { - var redirect = [10, "client:goback?"]; + headers += "\nwtv-noback-all: wtv-"; + headers += "\nwtv-expire-all: wtv-"; + var redirect = [5, "client:relogin?"]; var message = "Error: Your box is not registered. You are accessing " + minisrv_config.config.service_name + " in Guest Mode. There is nothing to delete!"; } else if (request_headers.query.confirm_unregister) { if (ssid_sessions[socket.ssid].unregisterBox()) { @@ -14,7 +16,7 @@ if (!ssid_sessions[socket.ssid].getSessionData("registered")) { } else { var redirect = [10, "client:goback?"]; var message = "There was an error deleting your account data. Please try again later. If the problem persists, please contact " + minisrv_config.config.service_owner + " to request manual deletion."; - message += "SSID verifcation may be required to perform a manual deletion.< br >
    Returning from whence you came...

    "; + message += "SSID verifcation may be required to perform a manual deletion.

    Returning from whence you came...

    "; message += `Click here if you are not automatically redirected.`; } } else { diff --git a/zefie_wtvp_minisrv/WTVClientSessionData.js b/zefie_wtvp_minisrv/WTVClientSessionData.js index d44c948d..1737eda2 100644 --- a/zefie_wtvp_minisrv/WTVClientSessionData.js +++ b/zefie_wtvp_minisrv/WTVClientSessionData.js @@ -4,55 +4,109 @@ class WTVClientSessionData { fs = require('fs'); path = require('path'); + ssid = null; data_store = null; session_store = null; login_security = null; capabilities = null; session_storage = ""; - hide_ssid_in_logs = true; + minisrv_config = []; + wtvshared = null; + wtvmime = null; - filterSSID(obj) { - if (this.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 { - if (obj["wtv-client-serial-number"]) { - var ssid = obj["wtv-client-serial-number"]; - if (ssid.substr(0, 8) == "MSTVSIMU") { - obj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20); - } else if (ssid.substr(0, 5) == "1SEGA") { - obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13); - } else { - obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9); - } - } - return obj; - } - } else { - return obj; - } - } + constructor(minisrv_config, ssid) { + if (!minisrv_config) throw ("minisrv_config required"); + var WTVShared = require('./WTVShared.js')['WTVShared']; + var WTVMime = require('./WTVMime.js'); + this.minisrv_config = minisrv_config; + this.wtvshared = new WTVShared(minisrv_config); + this.wtvmime = new WTVMime(minisrv_config); - constructor(ssid, hide_ssid_in_logs, session_storage_directory) { this.ssid = ssid; - if (hide_ssid_in_logs) this.hide_ssid_in_logs = hide_ssid_in_logs; - if (!session_storage_directory) session_storage_directory = __dirname + "/SessionStore"; - this.session_storage = session_storage_directory; this.data_store = new Array(); this.session_store = {}; } - getUTCTime(offset = 0) { - return new Date((new Date).getTime() + offset).toUTCString(); + /** + * Returns the absolute path to the user's file store, or false if unregistered + * @returns {string|boolean} Absolute path to the user's file store, or false if unregistered + */ + getUserStoreDirectory() { + if (!this.isRegistered()) return false; + return this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + this.path.sep; } + /** + * Store a file in the user's file store + * @param {string} path Relative path to User's file store + * @param {Buffer} data File data + * @param {number|null} last_modified Unix timestamp to set last modified date to + * @param {boolean} overwrite Overwrite if file exists + * @returns {boolean} Whether or not the file was written + */ + storeUserStoreFile(path, data, last_modified = null, overwrite = true) { + var store_dir = this.getUserStoreDirectory(); + if (!store_dir) return false; // unregistered + var result = false; + var path_split = path.split('/'); + var file_name = path_split.pop(); + var store_dir_path = this.wtvshared.makeSafePath(store_dir, path_split.join('/').replace('/', this.path.sep)); + var store_full_path = this.wtvshared.makeSafePath(store_dir_path, file_name); + + try { + if (!this.fs.existsSync(store_dir_path)) this.fs.mkdirSync(store_dir_path, { recursive: true }); + var file_exists = this.fs.existsSync(store_full_path); + if (!file_exists || (file_exists && overwrite)) result = this.fs.writeFileSync(store_full_path, data); + if (result !== false && last_modified) { + var file_timestamp = new Date(last_modified * 1000); + fs.utimesSync(store_full_path, Date.now(), file_timestamp) + } + } catch (e) { + console.error(" # User File Store failed", e); + } + return (result === false) ? false : true; + } + + /** + * Retrieves a file from the user store + * @param {string} path Path relative to the User File Store + * @returns {Buffer|false} Buffer data, or false if could not open file + */ + getUserStoreFile(path) { + var store_dir = this.getUserStoreDirectory(); + if (!store_dir) return false; // unregistered + var store_dir_path = this.wtvshared.makeSafePath(store_dir, path.replace('/', this.path.sep)); + if (this.fs.existsSync(store_dir_path)) return this.fs.readFileSync(store_dir_path); + else return false; + } + + /** + * Retrieves a file from the user store with a file://Disk/ url + * @param {string} url file://Disk/ base url + * @returns {Buffer|false} Buffer data, or false if could not open file + */ + getUserStoreFileByURL(url) { + var path_split = url.split('/'); + path_split.shift(); + path_split.shift(); + var store_dir_path = path_split.join('/').replace('/', this.path.sep); + return this.getUserStoreFile(store_dir_path); + } + + /** + * Retrieves the Content-Type of a User Store File + * @param {string} path Path relative to the User File Store + * @returns {string|false} Content-Type, or false if could not open file + */ + getUserStoreContentType(path) { + return this.wtvmime.getSimpleContentType(path); + } + + /** + * Returns the number of user cookies + * @returns {number} Number of cookies + */ countCookies() { return Object.keys(this.session_store.cookies).length || 0; } @@ -60,7 +114,7 @@ class WTVClientSessionData { 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.getUTCTime(365 * 86400000), "cookie_type=chocolatechip"); + this.addCookie("wtv.zefie.com", "/", this.wtvshared.getUTCTime(365 * 86400000), "cookie_type=chocolatechip"); } addCookie(domain, path = null, expires = null, data = null) { @@ -174,8 +228,8 @@ class WTVClientSessionData { loadSessionData(raw_data = false) { try { - if (this.fs.lstatSync(this.session_storage + this.path.sep + this.ssid + ".json")) { - var json_data = this.fs.readFileSync(this.session_storage + this.path.sep + this.ssid + ".json", 'Utf8') + if (this.fs.lstatSync(this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + ".json")) { + var json_data = this.fs.readFileSync(this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + ".json", 'Utf8') if (raw_data) return json_data; var session_data = JSON.parse(json_data); @@ -184,12 +238,12 @@ class WTVClientSessionData { } } catch (e) { // Don't log error 'file not found', it just means the client isn't registered yet - if (e.code != "ENOENT") console.error(" # Error loading session data for", this.filterSSID(this.ssid), e); + if (e.code != "ENOENT") console.error(" # Error loading session data for", this.wtvshared.filterSSID(this.ssid), e); return false; } } - saveSessionData() { + saveSessionData(force_write = false) { if (this.isRegistered()) { // load data from disk and merge new data var temp_store = this.session_store; @@ -198,17 +252,18 @@ class WTVClientSessionData { temp_store = null; } else { // do not write file if user is not registered, return true because this is not an error - return true; + // force write needed to set the initial reg + if (!force_write) return true; } try { // only save if file has changed var json_save_data = JSON.stringify(this.session_store); var json_load_data = this.loadSessionData(true); - if (json_save_data != json_load_data) this.fs.writeFileSync(this.session_storage + this.path.sep + this.ssid + ".json", JSON.stringify(this.session_store), "Utf8"); + if (json_save_data != json_load_data) this.fs.writeFileSync(this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + ".json", JSON.stringify(this.session_store), "Utf8"); return true; } catch (e) { - console.error(" # Error saving session data for", this.filterSSID(this.ssid), e); + console.error(" # Error saving session data for", this.wtvshared.filterSSID(this.ssid), e); return false; } } @@ -218,9 +273,9 @@ class WTVClientSessionData { return this.loadSessionData(); } - storeSessionData() { + storeSessionData(force_write = false) { // alias - return this.saveSessionData(); + return this.saveSessionData(force_write); } SaveIfRegistered() { @@ -231,7 +286,7 @@ class WTVClientSessionData { isRegistered() { var self = this; var ssid_match = false; - this.fs.readdirSync(this.session_storage).forEach(file => { + this.fs.readdirSync(this.minisrv_config.config.SessionStore).forEach(file => { if (!file.match(/.*\.json/ig)) return; if (ssid_match) return; if (file.split('.')[0] == self.ssid) ssid_match = true; @@ -240,15 +295,19 @@ class WTVClientSessionData { } unregisterBox() { + var user_store_base = this.wtvshared.makeSafePath(this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore), this.path.sep + this.ssid); try { - if (this.fs.lstatSync(this.session_storage + this.path.sep + this.ssid + ".json")) { - this.fs.unlinkSync(this.session_storage + this.path.sep + this.ssid + ".json"); + if (this.fs.existsSync(user_store_base + ".json")) { + this.fs.unlinkSync(user_store_base + ".json"); this.session_store = {}; - return true; } + if (this.fs.existsSync(user_store_base)) { + this.fs.rmdirSync(user_store_base, { recursive: true }); + } + return true; } catch (e) { // Don't log error 'file not found', it just means the client isn't registered yet - console.error(" # Error deleting session data for", this.filterSSID(this.ssid), e); + console.error(" # Error deleting session data for", this.wtvshared.filterSSID(this.ssid), e); return false; } } diff --git a/zefie_wtvp_minisrv/WTVDownloadList.js b/zefie_wtvp_minisrv/WTVDownloadList.js new file mode 100644 index 00000000..9daa7e45 --- /dev/null +++ b/zefie_wtvp_minisrv/WTVDownloadList.js @@ -0,0 +1,310 @@ +/** + * wtv/download-list creation helper class + * By: zefie + */ +class WTVDownloadList { + + download_list = ""; + service_name = ""; + content_type = "wtv/download-list"; + wtvshared = null; + clientShowAlert = null; + minisrv_config = []; + + /** + * Constructs the WTVDownloadList Class + * @param {string} service_name Service name to use in wtv-urls + */ + constructor(minisrv_config, service_name = "wtv-disk") { + var { WTVShared, clientShowAlert } = require('./WTVShared.js'); + this.minisrv_config = minisrv_config; + this.wtvshared = new WTVShared(minisrv_config); + this.clientShowAlert = clientShowAlert; + this.service_name = service_name + this.clear(); + } + + /** + * Clears the download list + */ + clear() { + this.download_list = ""; + } + + /** + * Alias to clear() (clears the download list) + */ + reset() { + this.clear(); + } + + /** + * Returns the download list. + * @returns {string} Download list for client; + */ + getDownloadList() { + return this.download_list; + } + + /** + * Adds a DISPLAY command to the download list + * @param {string} message Message to display to the client + */ + display(message) { + this.download_list += "DISPLAY " + message + "\n\n"; + } + + /** + * Adds an EXECUTE command to the download list + * @param {string} command client command to execute + */ + execute(command) { + this.download_list += "EXECUTE " + command + "\n\n"; + } + + /** + * Adds a CREATE partition command to the download list + * @param {string} path file://Disk/ path to desired partition + * @param {string} size Size of the desired partition + */ + createPartition(path, size) { + this.download_list += "CREATE " + path + "\n"; + this.download_list += "partition-size: " + size + "\n\n"; + } + + /** + * Adds a CREATE-GROUP command to the download list + * @param {string} name Group name + * @param {string} path file://Disk/ path of desired group + * @param {string} state Group state + * @param {boolean|null} service_owned Sets service owned flag. (null = don't set) + */ + createGroup(name, path, state = 'invalid', service_owned = null) { + this.download_list += "CREATE-GROUP " + name + "\n"; + this.download_list += "state: " + state + "\n"; + if (service_owned !== null) this.download_list += "service-owned: " + service_owned + "\n"; + this.download_list += "base: " + path + "\n\n"; + } + + /** + * An alias for createGroup() that handles creating the '-UPDATE' group for you + * @param {string} name Group name + * @param {string} path file://Disk/ path of desired group + * @param {string} state Group state + * @param {boolean} service_owned Sets service owned flag. + */ + createUpdateGroup(name, path, state = 'invalid', service_owned = false) { + this.createGroup(name + "-UPDATE", path, state); + this.createGroup(name, path, state, service_owned); + } + + /** + * Adds a DELETE command to the download list + * @param {string} path Non-absolute path of client destination file (relative to group base) if group defined, otherwise absolute file://Disk/ path to delete + * @param {string} group Group to which it belongs + */ + delete(path, group = null) { + path = this.wtvshared.stripGzipFromPath(path); + this.download_list += "DELETE " + path + "\n"; + if (group !== null) this.download_list += "group: " + group + "\n\n"; + } + + /** + * Adds a PUT command to the download list + * @param {string} path Absolute file://Disk/ path of a file to upload to the service + * @param {string} destination Destination address (wtv url on service) in which to POST upload the file to + */ + put(path, destination) { + this.download_list += "PUT " + path + "\n"; + this.download_list += "location: " + destination + "\n\n"; + } + + /** + * Alias to put() for User Store + * @param {string} path Absolute file://Disk/ path of a file to upload to the service + * @param {string} destination Destination file path in the User Store + */ + putUserStoreDest(path, destination) { + this.put(path, this.service_name + ":/userstore?partialPath=" + escape(destination)); + } + + /** + * Alias to putUserStoreDest() that generates the destination + * @param {any} path + */ + putUserStore(path) { + var destination = path.replace("file://", ""); + this.putUserStoreDest(path, destination); + } + /** + * Adds a GET command to the download list + * @param {string} file Non-absolute path of client destination file (relative to group base) + * @param {string} path Absolute file://Disk/ path of destination + * @param {string} source wtv-url to fetch file from + * @param {string} group Group this file belongs to + * @param {string} display Message to display while working on this file + * @param {string} checksum md5sum of the file + * @param {string} file_permission File permissions + */ + get(file, path, source, group, checksum = null, uncompressed_size = null, original_filename = null, file_permission = 'r') { + if (original_filename) { + file = file.split('/'); + var file_name = file[file.length - 1]; + path = path.replace(file_name, original_filename); + file.pop(); + if (file.length > 0) file = file.join('/') + '/' + original_filename; + else file = original_filename; + } + this.download_list += "GET " + file + "\n"; + + this.download_list += "group: " + group + "-UPDATE\n"; + this.download_list += "location: " + source + "\n"; + this.download_list += "file-permission: " + file_permission + "\n"; + if (checksum != null) this.download_list += "wtv-checksum: " + checksum + "\n"; + if (uncompressed_size != null) this.download_list += "wtv-uncompressed-filesize: " + uncompressed_size + "\n"; + this.download_list += "service-source-location: /webtv/content/" + source.substr(source.indexOf('-') + 1, source.indexOf(':/') - source.indexOf('-') - 1) + "d/" + source.substr(source.indexOf(':/') + 2) + "\n"; + this.download_list += "client-dest-location: " + path + "\n\n"; + } + + /** + * Adds a RENAME command to the download list + * @param {string} srcfile Non-absolute path of client source file (relative to source group base) + * @param {string} destfile Non-absolute path of client destination file (relative to destination group base) + * @param {string} srcgroup Source Group + * @param {string} destgroup Destination Group + */ + rename(srcfile, destfile, srcgroup, destgroup) { + srcfile = this.wtvshared.stripGzipFromPath(srcfile); + destfile = this.wtvshared.stripGzipFromPath(destfile); + this.download_list += "RENAME " + srcfile + "\n"; + this.download_list += "group: " + srcgroup + "-UPDATE\n"; + this.download_list += "destination-group: " + destgroup + "\n"; + this.download_list += "location: " + destfile + "\n\n"; + } + + /** + * Adds a SET-GROUP command to the download list + * @param {string} group Group to set state of + * @param {string} state State to set group to + * @param {string} version Version to set group to + */ + setGroup(group, state, version) { + this.download_list += "SET-GROUP " + group + "\n"; + this.download_list += "state: " + state + "\n"; + this.download_list += "version: " + version + "\n"; + this.download_list += "last-checkup-time: " + new Date().toUTCString().replace("GMT", "+0000") + "\n\n"; + } + + /** + * Adds a DELETE-GROUP command to the download list + * @param {string} group Group to delete + */ + deleteGroup(group) { + this.download_list += "DELETE-GROUP " + group + "\n\n"; + } + + /** + * An alias for deleteGroup() that handles deleting the '-UPDATE' group files for you + * @param {string} group Group to delete + * @param {string} path Group base path + */ + deleteGroupUpdate(group, path) { + this.deleteGroup(group + "-UPDATE"); + this.delete(path + ".GROUP-UPDATE/"); + } + + /** + * Generates the Download page + * @param {object} minisrv_config minisrv config object + * @param {string} title Page title + * @param {string} group + * @param {string|null} diskmap + * @param {string|null} main_message Message displayed in the center of the page + * @param {string|null} message Initial progress bar message + * @param {boolean|null} force_update Force this update even if the client reports the files are synced + * @param {string|null} success_url Where the client goes when the process succeeds + * @param {string|null} fail_url Where the client goes when the process fails. + * @param {string|null} url Use your own URL for client:fetch?source= instead of our generated one + * @returns {string} HTML Download Page + */ + getSyncPage(title, group, diskmap = null, main_message = null, message = null, force_update = null, success_url = null, fail_url = null, url = null) { + // Begin Set defaults + if (main_message === null) main_message = "Your receiver is downloading files."; + + if (message === null) message = "Retrieving files"; + + if (force_update === null) force_update = false; + + if (url === null) url = this.service_name + ":/sync?diskmap=" + escape(diskmap) + "&force=" + force_update; + + if (success_url === null) success_url = new this.clientShowAlert({ + 'image': this.minisrv_config.config.service_logo, + 'message': "Download successful!", + 'buttonlabel1': "Okay", + 'buttonaction1': "client:goback", + 'noback': true, + }).getURL(); + + if (fail_url === null) fail_url = new this.clientShowAlert({ + 'image': this.minisrv_config.config.service_logo, + 'message': "Download failed...", + 'buttonlabel1': "Fuck!", + 'buttonaction1': "client:goback", + 'noback': true, + }).getURL(); + // End set defaults + return ` + + + + ${title} + + + + + + + +
    + + + + + + +
    + ${message} +
    +
    +
    +
    + + + +
    + + + ${main_message} +

    This may take a while. + +

    + +

    + + + +
    + + + +` + } + +} + +module.exports = WTVDownloadList; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/WTVFlashrom.js b/zefie_wtvp_minisrv/WTVFlashrom.js index 986c4666..cf8b6c9e 100644 --- a/zefie_wtvp_minisrv/WTVFlashrom.js +++ b/zefie_wtvp_minisrv/WTVFlashrom.js @@ -5,16 +5,18 @@ class WTVFlashrom { use_zefie_server = true; bf0app_update = false; service_vaults = new Array(); + no_debug = false; service_name = ""; - zdebug = false; + minisrv_config = []; - constructor(service_vaults, service_name, use_zefie_server = true, bf0app_update = false, debug = false) { + constructor(minisrv_config, service_vaults, service_name, use_zefie_server = true, bf0app_update = false, no_debug = false) { this.service_vaults = service_vaults; this.service_name = service_name; this.use_zefie_server = use_zefie_server; this.bf0app_update = bf0app_update; - this.zdebug = true; + this.no_debug = no_debug; + this.minisrv_config = minisrv_config; } @@ -95,31 +97,37 @@ class WTVFlashrom { if (flashrom_info.magic == flashrom_magic) flashrom_info.valid_flashrom = true; if (!flashrom_info.valid_flashrom) console.error(" * Warning! FlashROM File Magic (" + flashrom_info.magic + ") did not match expected magic (" + flashrom_magic + ")..."); - if (this.zdebug) console.log(" # FlashROM File Magic (" + flashrom_info.magic + "), expected magic (" + flashrom_magic + "), OK = " + flashrom_info.valid_flashrom + "..."); + //if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # FlashROM File Magic (" + flashrom_info.magic + "), expected magic (" + flashrom_magic + "), OK = " + flashrom_info.valid_flashrom + "..."); flashrom_info.byte_progress = data.readUInt32BE(68); - if (this.zdebug) console.log(" # Flashrom Part Bytes Sent:", flashrom_info.byte_progress); flashrom_info.compression_type = parseInt(part_header[16], 16); - if (this.zdebug) console.log(" # Flashrom Part Compression Type:", flashrom_info.compression_type); + //if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Part Compression Type:", flashrom_info.compression_type); flashrom_info.part_data_size = data.readUInt32BE(4); - if (this.zdebug) console.log(" # Flashrom Part Data Size:", flashrom_info.part_data_size); + //if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Part Data Size:", flashrom_info.part_data_size); flashrom_info.part_total_size = flashrom_info.part_data_size + flashrom_info.header_length; - if (this.zdebug) console.log(" # Flashrom Part Total Size:", flashrom_info.part_total_size); - flashrom_info.total_parts_size = data.readUInt32BE(32); - if (this.zdebug) console.log(" # Flashrom All Parts Total Size:", flashrom_info.total_parts_size); + flashrom_info.percent_complete = ((((flashrom_info.byte_progress + flashrom_info.part_total_size) / flashrom_info.total_parts_size)) * 100).toFixed(1); + + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Part Size :", flashrom_info.part_total_size); + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Bytes Sent :", flashrom_info.byte_progress); + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Bytes Sent+:", flashrom_info.byte_progress + flashrom_info.part_total_size, "(" + flashrom_info.percent_complete + "% complete)"); + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Total Size :", flashrom_info.total_parts_size); // read current part number bit from part header flashrom_info.part_number = data.readUInt16BE(28); - if (this.zdebug) console.log(" # Flashrom Current Part Number:", flashrom_info.part_number); + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Curr Part Number :", flashrom_info.part_number); + flashrom_info.is_last_part = ((flashrom_info.byte_progress + flashrom_info.part_total_size) == flashrom_info.total_parts_size) ? true : false; + + if (flashrom_info.is_last_part) { + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Curr Part is Last:", flashrom_info.is_last_part); + } else { + flashrom_info.next_part_number = flashrom_info.part_number + 1; + if (this.minisrv_config.config.debug_flags.debug && !this.no_debug) console.log(" # Flashrom Next Part Number :", flashrom_info.next_part_number); + } // read current part display message from part header flashrom_info.message = new Buffer.from(part_header.toString('hex').substring(36 * 2, 68 * 2), 'hex').toString('ascii').replace(/[^0-9a-z\ \.\-]/gi, ""); - - flashrom_info.is_last_part = ((flashrom_info.byte_progress + flashrom_info.part_total_size) == flashrom_info.total_parts_size) ? true : false; flashrom_info.rompath = `wtv-flashrom:/${path}`; - if (this.zdebug) console.log(" # Flashrom Part Bytes Sent (after this part):", flashrom_info.byte_progress + flashrom_info.part_total_size); - if (this.zdebug) console.log(" # Flashrom Part is Last Part", flashrom_info.is_last_part); if (flashrom_info.is_last_part && this.bf0app_update) { flashrom_info.next_rompath = null; @@ -138,7 +146,7 @@ class WTVFlashrom { var flashrom_info = this.getFlashromInfo(data, request_path) if (flashrom_info.is_bootrom) headers += "Content-Type: binary/x-wtv-bootrom"; // maybe? else headers += "Content-Type: binary/x-wtv-flashblock"; - if (flashrom_info.next_rompath != null) headers += "\nwtv-visit: " + flashrom_info.next_rompath; + if (flashrom_info.next_rompath != null && this.bf0app_update) headers += "\nwtv-visit: " + flashrom_info.next_rompath; callback(data, headers); } @@ -179,7 +187,7 @@ class WTVFlashrom { }) res.on('end', function () { - console.log(` * Zefie's FlashROM Server HTTP Status: ${res.statusCode} ${res.statusMessage}`) + if (self.minisrv_config.config.debug_flags.debug) console.log(` * Zefie's FlashROM Server HTTP Status: ${res.statusCode} ${res.statusMessage}`) if (res.statusCode == 200) { var data = Buffer.from(data_hex, 'hex'); } else if (res.statusCode == 206) { diff --git a/zefie_wtvp_minisrv/WTVLzpf.js b/zefie_wtvp_minisrv/WTVLzpf.js index 5bfea2ea..09eb3cfd 100644 --- a/zefie_wtvp_minisrv/WTVLzpf.js +++ b/zefie_wtvp_minisrv/WTVLzpf.js @@ -400,8 +400,6 @@ class WTVLzpf { this.EncodeLiteral(code_length, code); } } - - return Buffer.from(this.encoded_data); } /** @@ -442,32 +440,26 @@ class WTVLzpf { // End this.AddByte((this.current_literal >>> 0x18) & 0xFF); this.AddByte(0x20); + + return Buffer.from(this.encoded_data); } /** * Converts the data to a Javascript Buffer object * - * @param data {String|Buffer|CryptoJS.lib.WordArray} Data to convert + * @param data {String|Buffer} Data to convert * * @returns {Buffer} Javascript Buffer object */ ConvertToBuffer(data) { - if (data.words) { - var WTVSec = require("./WTVSec.js"); - wtvsec = new WTVSec(1); - data = wtvsec.wordArrayToBuffer(data); - WTVSec, wtvsec = null; - } else if (!data.byteLength) { - // otherwise if its not already a Buffer, convert it to one - data = new Buffer.from(data); - } + data = new Buffer.from(data.toString('binary')); return data; } /** * Compress data using WebTV's Lzpf compression algorithm and adds the footer to the end. * - * @param uncompressed_data {String|Buffer|CryptoJS.lib.WordArray} data to compress + * @param uncompressed_data {String|Buffer} data to compress * * @returns {Buffer} Lzpf compression data */ @@ -475,9 +467,7 @@ class WTVLzpf { uncompressed_data = this.ConvertToBuffer(uncompressed_data); this.Begin(); this.EncodeBlock(uncompressed_data, true); - this.Finish(); - - return Buffer.from(this.encoded_data); + return this.Finish(); } } diff --git a/zefie_wtvp_minisrv/WTVMime.js b/zefie_wtvp_minisrv/WTVMime.js new file mode 100644 index 00000000..ac9e1eaf --- /dev/null +++ b/zefie_wtvp_minisrv/WTVMime.js @@ -0,0 +1,198 @@ +/** + * Simple class for WebTV Mime Types and overrides + */ + + +class WTVMime { + + mime = require('mime-types'); + wtvshared = null; + minisrv_config = []; + + + constructor(minisrv_config) { + var WTVShared = require('./WTVShared.js')['WTVShared']; + this.minisrv_config = minisrv_config; + this.wtvshared = new WTVShared(minisrv_config); + if (!String.prototype.reverse) { + String.prototype.reverse = function () { + var splitString = this.split(""); + var reverseArray = splitString.reverse(); + var joinArray = reverseArray.join(""); + return joinArray; + } + } + } + + + shouldWeCompress(ssid_session, headers_obj) { + var compress_data = false; + var compression_type = 0; // no compression + if (ssid_session) { + if (ssid_session.capabilities) { + if (ssid_session.capabilities['client-can-receive-compressed-data']) { + + if (this.minisrv_config.config.enable_lzpf_compression || this.minisrv_config.config.force_compression_type) { + compression_type = 1; // lzpf + } + + if (ssid_session) { + // if gzip is enabled... + if (this.minisrv_config.config.enable_gzip_compression || this.minisrv_config.config.force_compression_type) { + var is_bf0app = ssid_session.get("wtv-client-rom-type") == "bf0app"; + var is_minibrowser = (ssid_session.get("wtv-needs-upgrade") || ssid_session.get("wtv-used-8675309")); + var is_softmodem = ssid_session.get("wtv-client-rom-type").match(/softmodem/); + if (!is_bf0app && ((!is_softmodem && !is_minibrowser) || (is_softmodem && !is_minibrowser))) { + // softmodem boxes do not appear to support gzip in the minibrowser + // LC2 appears to support gzip even in the MiniBrowser + // LC2 and newer approms appear to support gzip + // bf0app does not appear to support gzip + compression_type = 2; // gzip + } + } + } + + + + // mostly for debugging + if (this.minisrv_config.config.force_compression_type == "lzpf") compression_type = 1; + if (this.minisrv_config.config.force_compression_type == "gzip") compression_type = 2; + + // do not compress if already encoded + if (headers_obj["Content-Encoding"]) return 0; + + // should we bother to compress? + var content_type = ""; + if (typeof (headers_obj) == 'string') content_type = headers_obj; + else content_type = (typeof (headers_obj["wtv-modern-content-type"]) != 'undefined') ? headers_obj["wtv-modern-content-type"] : headers_obj["Content-Type"]; + + if (content_type) { + // both lzpf and gzip + if (content_type.match(/^text\//) && content_type != "text/tellyscript") compress_data = true; + else if (content_type.match(/^application\/(x-?)javascript$/)) compress_data = true; + else if (content_type == "application/json") compress_data = true; + if (compression_type == 2) { + // gzip only + if (content_type.match(/^audio\/(x-)?[s3m|mod|xm]$/)) compress_data = true; // s3m, mod, xm + if (content_type.match(/^audio\/(x-)?[midi|wav|wave]$/)) compress_data = true; // midi & wav + if (content_type.match(/^binary\/x-wtv-approm$/)) compress_data = true; // approms + if (content_type.match(/^binary\/doom-data$/)) compress_data = true; // DOOM WADs + } + } + } + } + } + + // return compression_type if compress_data = true + return (compress_data) ? compression_type : 0; + } + + /** + * Gets the WebTV Content-Type + * @param {string} path Path to a file + * @returns {string} Content-Type + */ + getSimpleContentType(path) { + return this.getContentType(path)[0]; + } + + /** + * Gets both the WebTV Content-Type and the Modern Content-Type + * @param {string} path Path to a file + * @returns {Array} (WebTV Content-Type, Modern Content-Type) + */ + getContentType(path) { + var file_ext = this.wtvshared.getFileExt(path).toLowerCase(); + var wtv_mime_type = ""; + var modern_mime_type = ""; + // process WebTV overrides, fall back to generic mime lookup + switch (file_ext) { + case "aif": + wtv_mime_type = "audio/x-aif"; + break; + case "aifc": + wtv_mime_type = "audio/x-aifc"; + break; + case "aiff": + wtv_mime_type = "audio/x-aiff"; + break; + case "ani": + wtv_mime_type = "x-wtv-animation"; + break; + case "brom": + wtv_mime_type = "binary/x-wtv-bootrom"; + break; + case "cdf": + wtv_mime_type = "application/netcdf"; + break; + case "dat": + wtv_mime_type = "binary/cache-data"; + break; + case "dl": + wtv_mime_type = "wtv/download-list"; + break; + case "gsm": + wtv_mime_type = "audio/x-gsm"; + break; + case "gz": + wtv_mime_type = "application/gzip"; + break; + case "ini": + wtv_mime_type = "wtv/jack-configuration"; + break; + case "mips-code": + wtv_mime_type = "code/x-wtv-code-mips"; + break; + case "o": + wtv_mime_type = "binary/x-wtv-approm"; + break; + case "ram": + wtv_mime_type = "audio/x-pn-realaudio"; + break; + case "rom": + wtv_mime_type = "binary/x-wtv-flashblock"; + break; + case "rsp": + wtv_mime_type = "wtv/jack-response"; + break; + case "swa": + case "swf": + wtv_mime_type = "application/x-shockwave-flash"; + break; + case "srf": + case "spl": + wtv_mime_type = "wtv/jack-data"; + break; + case "ttf": + wtv_mime_type = "wtv/jack-fonts"; + break; + case "tvch": + wtv_mime_type = "wtv/tv-channels"; + break; + case "tvl": + wtv_mime_type = "wtv/tv-listings"; + break; + case "tvsl": + wtv_mime_type = "wtv/tv-smartlinks"; + break; + case "wad": + wtv_mime_type = "binary/doom-data"; + break; + case "mp2": + case "hsb": + case "rmf": + case "s3m": + case "mod": + case "xm": + wtv_mime_type = "application/Music"; + break; + } + + modern_mime_type = this.mime.lookup(path); + if (wtv_mime_type == "") wtv_mime_type = modern_mime_type; + return new Array(wtv_mime_type, modern_mime_type); + } + +} + +module.exports = WTVMime; diff --git a/zefie_wtvp_minisrv/WTVSec.js b/zefie_wtvp_minisrv/WTVSec.js index cbdfe98a..a73a5c31 100644 --- a/zefie_wtvp_minisrv/WTVSec.js +++ b/zefie_wtvp_minisrv/WTVSec.js @@ -31,19 +31,18 @@ class WTVSec { hRC4_Key1 = null; hRC4_Key2 = null; RC4Session = new Array(); - zdebug = false; - + minisrv_config = []; /** * * Initialize the WTVSec class. * * @param {Number} wtv_incarnation Sets the wtv-incarnation for this instance - * @param {Boolean} zdebug Enable debugging + * @param {Boolean} minisrv_config.config.debug_flags.debug Enable debugging * */ - constructor(wtv_incarnation = 1, zdebug = false) { - this.zdebug = zdebug; + constructor(minisrv_config, wtv_incarnation = 1) { + this.minisrv_config = minisrv_config; this.initial_shared_key = CryptoJS.enc.Base64.parse(this.initial_shared_key_b64); if (this.initial_shared_key.sigBytes === 8) { @@ -238,7 +237,8 @@ class WTVSec { * #returns {Buffer} JS Buffer object */ wordArrayToBuffer(wordArray) { - return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex'); + if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex'); + else return null; } /** @@ -247,7 +247,7 @@ class WTVSec { * */ SecureOn(rc4session = null) { - if (this.zdebug) console.log(" # Generating RC4 sessions with wtv-incarnation: " + this.incarnation); + if (this.minisrv_config.config.debug_flags.debug) console.log(" # Generating RC4 sessions with wtv-incarnation: " + this.incarnation); var buf = new Uint8Array([0xff & this.incarnation, 0xff & (this.incarnation >> 8), 0xff & (this.incarnation >> 16), 0xff & (this.incarnation >> 24)]); endianness(buf, 4); diff --git a/zefie_wtvp_minisrv/WTVShared.js b/zefie_wtvp_minisrv/WTVShared.js new file mode 100644 index 00000000..f3d6493d --- /dev/null +++ b/zefie_wtvp_minisrv/WTVShared.js @@ -0,0 +1,195 @@ +/** + * Shared functions across all classes and apps + */ + +class WTVShared { + + path = require('path'); + fs = require('fs'); + minisrv_config = []; + + constructor(minisrv_config) { + this.minisrv_config = minisrv_config; + if (!String.prototype.reverse) { + String.prototype.reverse = function () { + var splitString = this.split(""); + var reverseArray = splitString.reverse(); + var joinArray = reverseArray.join(""); + return joinArray; + } + } + } + + /** + * Returns the Last-Modified date in Unix Timestamp format + * @param {string} file Path to a file + */ + getFileLastModified(file) { + var stats = this.fs.lstatSync(file); + if (stats) return new Date(stats.mtimeMs); + return false; + } + + /** + * Returns the Last-Modified date in a RFC7231 compliant UTC Date String + * @param {string} file Path to a file + */ + getFileLastModifiedUTCString(file) { + return this.getFileLastModified(file).toUTCString(); + } + + /** + * Returns a RFC7231 compliant UTC Date String from the current time + * @param {Number} offset Offset from current time (+/-) + * @returns {string} A RFC7231 compliant UTC Date String from the current time + */ + getUTCTime(offset = 0) { + return new Date((new Date).getTime() + offset).toUTCString(); + } + + /** + * Returns a censored SSID + * @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 { + if (obj["wtv-client-serial-number"]) { + var ssid = obj["wtv-client-serial-number"]; + if (ssid.substr(0, 8) == "MSTVSIMU") { + obj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20); + } else if (ssid.substr(0, 5) == "1SEGA") { + obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13); + } else { + obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9); + } + } + return obj; + } + } else { + return obj; + } + } + + /** + * Returns an absolute path + * @param {string} path + * @param {string} directory Root directory + */ + getAbsolutePath(path, directory = __dirname) { + if (path.substring(0, 1) != this.path.sep && path.substring(1, 1) != ":") { + // non-absolute path, so use current directory as base + path = (directory + this.path.sep + path); + } else { + // already absolute path + } + return path; + } + + /** + * Returns a percentage + * @param {number} partialValue + * @param {number} totalValue + * @returns {number} percentage + */ + getPercentage = function (partialValue, totalValue) { + return Math.floor((100 * partialValue) / totalValue); + } + + /** + * If the file ends with .gz, remove it + * @param {string} path + * @return {string} path without gz, or unmodified path if it isnt a gz + */ + stripGzipFromPath(path) { + var path_split = path.split('.'); + if (path_split[path_split.length - 1].toLowerCase() == "gz") { + path_split.pop(); + path = path_split.join("."); + } + return path; + } + + /** + * Gets the file extension from a path + * @param {string} path + * @returns {String} File Extension (without dot) + */ + getFileExt(path) { + return path.reverse().split(".")[0].reverse(); + } + + /** + * Strips bad things from paths + * @param {string} base Base path + * @param {string} target Sub path + */ + makeSafePath(base, target) { + target.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, ""); + if (this.path.sep != "/") target = target.replace(/\//g, this.path.sep); + var targetPath = this.path.posix.normalize(target) + return base + this.path.sep + targetPath; + } + + /** + * Makes sure an SSID is clean, and doesn't contain any exploitable characters + * @param {string} ssid + * @returns {string} Sanitized SSID + */ + makeSafeSSID(ssid = "") { + ssid = ssid.replace(/[^a-zA-Z0-9]/g, ""); + if (ssid.length == 0) ssid = null; + return ssid; + } +} + +class clientShowAlert { + message = null; + buttonlabel1 = null; + buttonlabel2 = null; + buttonaction1 = null; + buttonaction2 = null; + noback = null; + image = null; + + constructor(image = null, message = null, buttonlabel1 = null, buttonaction1 = null, buttonlabel2 = null, buttonaction2 = null, noback = null) { + this.message = message; + this.buttonlabel1 = buttonlabel1; + this.buttonlabel2 = buttonlabel2; + this.buttonaction1 = buttonaction1; + this.buttonaction2 = buttonaction2; + this.message = message; + this.noback = noback; + if (typeof image === 'object') { + this.image = null; + Object.keys(image).forEach(function (k) { + if (this[k] === null) this[k] = image[k]; + }, this); + } else { + this.image = image; + } + } + + getURL() { + var url = "client:ShowAlert?"; + if (this.message) url += "message=" + escape(this.message) + "&"; + if (this.buttonlabel1) url += "buttonlabel1=" + escape(this.buttonlabel1) + "&"; + if (this.buttonaction1) url += "buttonaction1=" + escape(this.buttonaction1) + "&"; + if (this.buttonlabel2) url += "buttonlabel2=" + escape(this.buttonlabel2) + "&"; + if (this.buttonaction2) url += "buttonaction2=" + escape(this.buttonaction2) + "&"; + if (this.image) url += "image=" + escape(this.image) + "&"; + if (this.noback) url += "noback=true&"; + return url.substring(0, url.length - 1); + } +} + +module.exports.WTVShared = WTVShared; +module.exports.clientShowAlert = clientShowAlert; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index ac1624bb..c7810f1b 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -8,13 +8,14 @@ const https = require('https'); const strftime = require('strftime'); // used externally by service scripts const net = require('net'); const CryptoJS = require('crypto-js'); -const mime = require('mime-types'); const { crc16 } = require('easy-crc'); const process = require('process'); var WTVSec = require('./WTVSec.js'); var WTVLzpf = require('./WTVLzpf.js'); var WTVClientCapabilities = require('./WTVClientCapabilities.js'); var WTVClientSessionData = require('./WTVClientSessionData.js'); +var WTVMime = require("./WTVMime.js"); +var { WTVShared, clientShowAlert } = require("./WTVShared.js"); process .on('SIGTERM', shutdown('SIGTERM')) @@ -70,10 +71,6 @@ function getServiceString(service, overrides = {}) { } } -function getFileExt(path) { - return path.reverse().split(".")[0].reverse(); -} - function doErrorPage(code, data = null, pc_mode = false) { var headers = null; switch (code) { @@ -105,99 +102,6 @@ function doErrorPage(code, data = null, pc_mode = false) { return new Array(headers, data); } - -function getConType(path) { - var file_ext = getFileExt(path).toLowerCase(); - var wtv_mime_type = ""; - var modern_mime_type = ""; - // process WebTV overrides, fall back to generic mime lookup - switch (file_ext) { - case "aif": - wtv_mime_type = "audio/x-aif"; - break; - case "aifc": - wtv_mime_type = "audio/x-aifc"; - break; - case "aiff": - wtv_mime_type = "audio/x-aiff"; - break; - case "ani": - wtv_mime_type = "x-wtv-animation"; - break; - case "brom": - wtv_mime_type = "binary/x-wtv-bootrom"; - break; - case "cdf": - wtv_mime_type = "application/netcdf"; - break; - case "dat": - wtv_mime_type = "binary/cache-data"; - break; - case "dl": - wtv_mime_type = "wtv/download-list"; - break; - case "gsm": - wtv_mime_type = "audio/x-gsm"; - break; - case "gz": - wtv_mime_type = "application/gzip"; - break; - case "ini": - wtv_mime_type = "wtv/jack-configuration"; - break; - case "mips-code": - wtv_mime_type = "code/x-wtv-code-mips"; - break; - case "o": - wtv_mime_type = "binary/x-wtv-approm"; - break; - case "ram": - wtv_mime_type = "audio/x-pn-realaudio"; - break; - case "rom": - wtv_mime_type = "binary/x-wtv-flashblock"; - break; - case "rsp": - wtv_mime_type = "wtv/jack-response"; - break; - case "swa": - case "swf": - wtv_mime_type = "application/x-shockwave-flash"; - break; - case "srf": - case "spl": - wtv_mime_type = "wtv/jack-data"; - break; - case "ttf": - wtv_mime_type = "wtv/jack-fonts"; - break; - case "tvch": - wtv_mime_type = "wtv/tv-channels"; - break; - case "tvl": - wtv_mime_type = "wtv/tv-listings"; - break; - case "tvsl": - wtv_mime_type = "wtv/tv-smartlinks"; - break; - case "wad": - wtv_mime_type = "binary/doom-data"; - break; - case "mp2": - case "hsb": - case "rmf": - case "s3m": - case "mod": - case "xm": - wtv_mime_type = "application/Music"; - break; - } - - modern_mime_type = mime.lookup(path); - if (wtv_mime_type == "") wtv_mime_type = modern_mime_type; - return new Array(wtv_mime_type, modern_mime_type); -} - async function processPath(socket, service_vault_file_path, request_headers = new Array(), service_name) { var headers, data = null; var request_is_async = false; @@ -206,7 +110,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne try { service_vaults.forEach(function (service_vault_dir) { if (service_vault_found) return; - service_vault_file_path = makeSafePath(service_vault_dir, service_path); + service_vault_file_path = wtvshared.makeSafePath(service_vault_dir, service_path); // deny access to catchall file name directly var service_path_split = service_path.split("/"); @@ -230,8 +134,9 @@ async function processPath(socket, service_vault_file_path, request_headers = ne // file exists, read it and return it service_vault_found = true; request_is_async = true; - if (!zquiet) console.log(" * Found " + service_vault_file_path + " to handle request (Direct File Mode) [Socket " + socket.id + "]"); - var contypes = getConType(service_vault_file_path); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Found " + service_vault_file_path + " to handle request (Direct File Mode) [Socket " + socket.id + "]"); + request_headers.service_file_path = service_vault_file_path; + var contypes = wtvmime.getContentType(service_vault_file_path); headers = "200 OK\n" headers += "Content-Type: " + contypes[0] + "\n"; headers += "wtv-modern-content-type" + contypes[1]; @@ -241,8 +146,9 @@ async function processPath(socket, service_vault_file_path, request_headers = ne } else if (fs.existsSync(service_vault_file_path + ".txt")) { // raw text format, entire payload expected (headers and content) service_vault_found = true; - if (!zquiet) console.log(" * Found " + service_vault_file_path + ".txt to handle request (Raw TXT Mode) [Socket " + socket.id + "]"); request_is_async = true; + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Found " + service_vault_file_path + ".txt to handle request (Raw TXT Mode) [Socket " + socket.id + "]"); + request_headers.service_file_path = service_vault_file_path + ".txt"; fs.readFile(service_vault_file_path + ".txt", 'Utf-8', function (err, file_raw) { if (file_raw.indexOf("\n\n") > 0) { // split headers and data by newline (unix format) @@ -272,18 +178,20 @@ async function processPath(socket, service_vault_file_path, request_headers = ne // In Asynchronous mode, you are expected to call sendToClient(socket,headers,data) by the end of your script // `socket` is already defined and should be passed-through. service_vault_found = true; - if (!zquiet) console.log(" * Found " + service_vault_file_path + ".js to handle request (JS Interpreter mode) [Socket " + socket.id + "]"); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Found " + service_vault_file_path + ".js to handle request (JS Interpreter mode) [Socket " + socket.id + "]"); + request_headers.service_file_path = service_vault_file_path + ".js"; // expose var service_dir for script path to the root of the wtv-service var service_dir = service_vault_dir + path.sep + service_name; socket_sessions[socket.id].starttime = Math.floor(new Date().getTime() / 1000); var jscript_eval = fs.readFileSync(service_vault_file_path + ".js").toString(); eval(jscript_eval); - if (request_is_async && !zquiet) console.log(" * Script requested Asynchronous mode"); + if (request_is_async && !minisrv_config.config.debug_flags.quiet) console.log(" * Script requested Asynchronous mode"); } else if (fs.existsSync(service_vault_file_path + ".html")) { // Standard HTML with no headers, WTV Style service_vault_found = true; - if (!zquiet) console.log(" * Found " + service_vault_file_path + ".html to handle request (HTML Mode) [Socket " + socket.id + "]"); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Found " + service_vault_file_path + ".html to handle request (HTML Mode) [Socket " + socket.id + "]"); + request_headers.service_file_path = service_vault_file_path + ".html"; request_is_async = true; headers = "200 OK\n" headers += "Content-Type: text/html" @@ -303,12 +211,13 @@ async function processPath(socket, service_vault_file_path, request_headers = ne while (service_check_dir.join(path.sep) != service_vault_dir) { var catchall_file = service_check_dir.join(path.sep) + path.sep + minisrv_catchall_file_name; if (fs.existsSync(catchall_file)) { - if (!zquiet) console.log(" * Found catchall at " + catchall_file + ".html to handle request (HTML Mode) [Socket " + socket.id + "]"); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Found catchall at " + catchall_file + " to handle request (JS Interpreter Mode) [Socket " + socket.id + "]"); + request_headers.service_file_path = catchall_file; var jscript_eval = fs.readFileSync(catchall_file).toString(); // don't pass these vars to the script var service_check_dir, minisrv_catchall_file_name = null; eval(jscript_eval); - if (request_is_async && !zquiet) console.log(" * Script requested Asynchronous mode"); + if (request_is_async && !minisrv_config.config.debug_flags.quiet) console.log(" * Script requested Asynchronous mode"); } else { service_check_dir.pop(); } @@ -345,47 +254,6 @@ async function processPath(socket, service_vault_file_path, request_headers = ne } } -function filterSSID(obj) { - if (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 { - if (makeSafeSSID(obj["wtv-client-serial-number"])) { - var ssid = makeSafeSSID(obj["wtv-client-serial-number"]); - if (ssid.substr(0, 8) == "MSTVSIMU") { - obj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20); - } else if (ssid.substr(0, 5) == "1SEGA") { - obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13); - } else { - obj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9); - } - } - return obj; - } - } else { - return obj; - } -} - -function makeSafeSSID(ssid = "") { - ssid = ssid.replace(/[^a-zA-Z0-9]/g, ""); - if (ssid.length == 0) ssid = null; - return ssid; -} - -function makeSafePath(base, target) { - target.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, ""); - if (path.sep != "/") target = target.replace(/\//g, path.sep); - var targetPath = path.posix.normalize(target) - return base + path.sep + targetPath; -} - async function processURL(socket, request_headers) { var shortURL, headers, data = ""; request_headers.query = new Array(); @@ -406,30 +274,36 @@ async function processURL(socket, request_headers) { } else { shortURL = unescape(request_headers.request_url); } + if (request_headers['wtv-request-type']) socket_sessions[socket.id].wtv_request_type = request_headers['wtv-request-type']; if (request_headers.post_data) { - var post_data_string = request_headers.post_data.toString(CryptoJS.enc.Utf8).replace("\0", ""); - if (isUnencryptedString(post_data_string)) { - if (post_data_string.indexOf('=')) { - if (post_data_string.indexOf('&')) { - var qraw = post_data_string.split('&'); - if (qraw.length > 0) { - for (let i = 0; i < qraw.length; i++) { - var qraw_split = qraw[i].split("="); - if (qraw_split.length == 2) { - var k = qraw_split[0]; - request_headers.query[k] = unescape(qraw[i].split("=")[1].replace(/\+/g, "%20")); + var post_data_string = ''; + try { + post_data_string = request_headers.post_data.toString(CryptoJS.enc.Utf8).replace("\0", ""); // if not text this will probably throw an exception + if (isUnencryptedString(post_data_string)) { + if (post_data_string.indexOf('=')) { + if (post_data_string.indexOf('&')) { + var qraw = post_data_string.split('&'); + if (qraw.length > 0) { + for (let i = 0; i < qraw.length; i++) { + var qraw_split = qraw[i].split("="); + if (qraw_split.length == 2) { + var k = qraw_split[0]; + request_headers.query[k] = unescape(qraw[i].split("=")[1].replace(/\+/g, "%20")); + } } } - } - } else { - var qraw_split = post_data_string.split("="); - if (qraw_split.length == 2) { - var k = qraw_split[0]; - request_headers.query[k] = unescape(qraw_split[1].replace(/\+/g, "%20")); + } else { + var qraw_split = post_data_string.split("="); + if (qraw_split.length == 2) { + var k = qraw_split[0]; + request_headers.query[k] = unescape(qraw_split[1].replace(/\+/g, "%20")); + } } } } + } catch (e) { + // do nothing } } @@ -469,26 +343,22 @@ async function processURL(socket, request_headers) { var ssid = socket.ssid; if (ssid == null) { // prevent possible injection attacks via SSID and filesystem SessionStore - ssid = makeSafeSSID(request_headers["wtv-client-serial-number"]); + ssid = wtvshared.makeSafeSSID(request_headers["wtv-client-serial-number"]); if (ssid == "") ssid = null; } var reqverb = "Request"; - if (request_headers.encrypted || request_headers.secure) { - reqverb = "Encrypted " + reqverb; - } - if (request_headers.psuedo_encryption) { - reqverb = "Psuedo-encrypted " + reqverb; - } + if (request_headers.encrypted || request_headers.secure) reqverb = "Encrypted " + reqverb; if (ssid != null) { - console.log(" * " + reqverb + " for " + request_headers.request_url + " from WebTV SSID " + (await filterSSID(ssid)), 'on', socket.id); + console.log(" * " + reqverb + " for " + request_headers.request_url + " from WebTV SSID " + (await wtvshared.filterSSID(ssid)), 'on', socket.id); } else { console.log(" * " + reqverb + " for " + request_headers.request_url, 'on', socket.id); } // assume webtv since there is a :/ in the GET var service_name = shortURL.split(':/')[0]; var urlToPath = service_name + path.sep + shortURL.split(':/')[1]; - if (zshowheaders) console.log(" * Incoming headers on socket ID", socket.id, (await filterSSID(request_headers))); + if (minisrv_config.config.debug_flags.show_headers) console.log(" * Incoming headers on socket ID", socket.id, (await wtvshared.filterSSID(request_headers))); + socket_sessions[socket.id].request_headers = request_headers; processPath(socket, urlToPath, request_headers, service_name); } else if (shortURL.indexOf('http://') >= 0 || shortURL.indexOf('https://') >= 0) { doHTTPProxy(socket, request_headers); @@ -505,7 +375,7 @@ async function processURL(socket, request_headers) { async function doHTTPProxy(socket, request_headers) { var request_type = (request_headers.request_url.substring(0, 5) == "https") ? "https" : "http"; - if (zshowheaders) console.log(request_type.toUpperCase() +" Proxy: Client Request Headers on socket ID", socket.id, (await filterSSID(request_headers))); + if (minisrv_config.config.debug_flags.show_headers) console.log(request_type.toUpperCase() +" Proxy: Client Request Headers on socket ID", socket.id, (await wtvshared.filterSSID(request_headers))); switch (request_type) { case "https": var proxy_agent = https; @@ -670,68 +540,6 @@ function headerStringToObj(headers, response = false) { return headers_obj; } -function shouldWeCompress(ssid, headers_obj) { - var compress_data = false; - var compression_type = 0; // no compression - if (ssid_sessions[ssid]) { - if (ssid_sessions[ssid].capabilities) { - if (ssid_sessions[ssid].capabilities['client-can-receive-compressed-data']) { - - if (minisrv_config.config.enable_lzpf_compression || minisrv_config.config.force_compression_type) { - compression_type = 1; // lzpf - } - - if (ssid_sessions[ssid]) { - // if gzip is enabled... - if (minisrv_config.config.enable_gzip_compression || minisrv_config.config.force_compression_type) { - var is_bf0app = ssid_sessions[ssid].get("wtv-client-rom-type") == "bf0app"; - var is_minibrowser = (ssid_sessions[ssid].get("wtv-needs-upgrade") || ssid_sessions[ssid].get("wtv-used-8675309")); - var is_softmodem = ssid_sessions[ssid].get("wtv-client-rom-type").match(/softmodem/); - if (!is_bf0app && ((!is_softmodem && !is_minibrowser) || (is_softmodem && !is_minibrowser))) { - // softmodem boxes do not appear to support gzip in the minibrowser - // LC2 appears to support gzip even in the MiniBrowser - // LC2 and newer approms appear to support gzip - // bf0app does not appear to support gzip - compression_type = 2; // gzip - } - } - } - - - - // mostly for debugging - if (minisrv_config.config.force_compression_type == "lzpf") compression_type = 1; - if (minisrv_config.config.force_compression_type == "gzip") compression_type = 2; - - // do not compress if already encoded - if (headers_obj["Content-Encoding"]) return 0; - - // should we bother to compress? - var content_type = ""; - if (typeof (headers_obj) == 'string') content_type = headers_obj; - else content_type = (typeof (headers_obj["wtv-modern-content-type"]) != 'undefined') ? headers_obj["wtv-modern-content-type"] : headers_obj["Content-Type"]; - - if (content_type) { - // both lzpf and gzip - if (content_type.match(/^text\//) && content_type != "text/tellyscript") compress_data = true; - else if (content_type.match(/^application\/(x-?)javascript$/)) compress_data = true; - else if (content_type == "application/json") compress_data = true; - if (compression_type == 2) { - // gzip only - if (content_type.match(/^audio\/(x-)?[s3m|mod|xm]$/)) compress_data = true; // s3m, mod, xm - if (content_type.match(/^audio\/(x-)?[midi|wav|wave]$/)) compress_data = true; // midi & wav - if (content_type.match(/^binary\/x-wtv-approm$/)) compress_data = true; // midi & wav - - } - } - } - } - } - - // return compression_type if compress_data = true - return (compress_data) ? compression_type : 0; -} - async function sendToClient(socket, headers_obj, data) { var headers = ""; var content_length = 0; @@ -766,13 +574,39 @@ async function sendToClient(socket, headers_obj, data) { delete headers_obj["Content-type"]; } + // Add last modified if not a dynamic script + if (socket_sessions[socket.id]) { + if (socket_sessions[socket.id].request_headers) { + if (wtvshared.getFileExt(socket_sessions[socket.id].request_headers.service_file_path).toLowerCase() !== "js") { + var last_modified = wtvshared.getFileLastModifiedUTCString(socket_sessions[socket.id].request_headers.service_file_path); + if (last_modified) headers_obj["Last-Modified"] = last_modified; + } + } + } + + if (content_length > 0) { + if (socket_sessions[socket.id].wtv_request_type == "download") { + if (headers_obj['Content-Type'] != "wtv/download-list") { + if (wtvshared.getFileExt(socket_sessions[socket.id].request_headers.request_url).toLowerCase() == "gz") { + // we need the checksum of the uncompressed data + var gunzipped = zlib.gunzipSync(data); + headers_obj['wtv-checksum'] = CryptoJS.MD5(CryptoJS.lib.WordArray.create(gunzipped)).toString(CryptoJS.enc.Hex).toLowerCase(); + headers_obj['wtv-uncompressed-size'] = gunzipped.byteLength; + gunzipped = null; + } else { + headers_obj['wtv-checksum'] = CryptoJS.MD5(CryptoJS.lib.WordArray.create(data)).toString(CryptoJS.enc.Hex).toLowerCase(); + } + } + } + } + // if box can do compression, see if its worth enabling // small files actually get larger, so don't compress them var compression_type = 0; - if (content_length >= 256) compression_type = shouldWeCompress(socket.ssid, headers_obj); + if (content_length >= 256) compression_type = wtvmime.shouldWeCompress(ssid_sessions[socket.ssid], headers_obj); // compress if needed - if (compression_type > 0 && content_length > 0 && headers_obj['http_response'].substring(0,3) == "200") { + if (compression_type > 0 && content_length > 0 && headers_obj['http_response'].substring(0, 3) == "200") { var uncompressed_content_length = content_length; switch (compression_type) { case 1: @@ -801,21 +635,27 @@ async function sendToClient(socket, headers_obj, data) { // ultimately send original content length if lzpf compressed_content_length = data.byteLength; } - var compression_percentage = ((compressed_content_length / uncompressed_content_length) * 100).toFixed(1).toString() + "%"; - if (uncompressed_content_length != compressed_content_length) if (zdebug) console.log(" # Compression stats: Orig Size:", uncompressed_content_length, "~ Comp Size:", compressed_content_length, "~ Ratio:", compression_percentage); + var compression_ratio = (uncompressed_content_length / compressed_content_length).toFixed(2); + var compression_percentage = ((1 - (compressed_content_length / uncompressed_content_length)) * 100).toFixed(1); + if (uncompressed_content_length != compressed_content_length) if (minisrv_config.config.debug_flags.debug) console.log(" # Compression stats: Orig Size:", uncompressed_content_length, "~ Comp Size:", compressed_content_length, "~ Ratio:", compression_ratio, "Saved:", compression_percentage.toString() + "%"); } // encrypt if needed - if (socket_sessions[socket.id].secure == true) { + if (socket_sessions[socket.id].secure == true && !socket_sessions[socket.id].do_not_encrypt) { headers_obj["wtv-encrypted"] = 'true'; headers_obj = moveObjectElement('wtv-encrypted', 'Connection', headers_obj); if (content_length > 0 && socket_sessions[socket.id].wtvsec) { - if (!zquiet) console.log(" * Encrypting response to client ...") + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Encrypting response to client ...") var enc_data = socket_sessions[socket.id].wtvsec.Encrypt(1, data); data = enc_data; } } + if (socket_sessions[socket.id].do_not_encrypt) { + if (headers_obj["wtv-encrypted"]) delete headers_obj["wtv-encrypted"]; + if (headers_obj["secure"]) delete headers_obj["secure"]; + } + // calculate content length // make sure we are using our Content-length and not one set in a script. if (headers_obj["Content-Length"]) delete headers_obj["Content-Length"]; @@ -841,8 +681,23 @@ async function sendToClient(socket, headers_obj, data) { headers_obj['http_response'] = "HTTP/1.0 " + headers_obj['http_response']; } +/* // wtv-request-type download wants minimal headers? + if (data.byteLength > 0) { + if (socket_sessions[socket.id].wtv_request_type == "download") { + if (headers_obj['Content-Type'] != "wtv/download-list") { + // minimalize headers + var new_headers = { "http_response": headers_obj['http_response'].split(" ")[0] + " " } + if (headers_obj['wtv-encrypted']) new_headers['wtv-encrypted'] = headers_obj['wtv-encrypted']; + new_headers["content-type"] = headers_obj['Content-Type']; + new_headers["content-length"] = headers_obj['Content-length']; + + headers_obj = new_headers; + } + } + } +*/ // header object to string - if (zshowheaders) console.log(" * Outgoing headers on socket ID", socket.id, (await filterSSID(headers_obj))); + if (minisrv_config.config.debug_flags.show_headers) console.log(" * Outgoing headers on socket ID", socket.id, (await wtvshared.filterSSID(headers_obj))); Object.keys(headers_obj).forEach(function (k) { if (k == "http_response") { headers += headers_obj[k] + end_of_line; @@ -856,40 +711,67 @@ async function sendToClient(socket, headers_obj, data) { } }); + if (headers_obj["Connection"]) { + if (headers_obj["Connection"].toLowerCase() == "close" && wtv_connection_close == "true") { + socket_sessions[socket.id].destroy_me = true; + } + } // send to client var toClient = null; if (typeof data == 'string') { toClient = headers + end_of_line + data; - socket.write(toClient); + sendToSocket(socket, Buffer.from(toClient)); } else if (typeof data == 'object') { - if (zquiet) var verbosity_mod = (headers_obj["wtv-encrypted"] == 'true') ? " encrypted response" : ""; + if (minisrv_config.config.debug_flags.quiet) var verbosity_mod = (headers_obj["wtv-encrypted"] == 'true') ? " encrypted response" : ""; if (socket_sessions[socket.id].secure_headers == true) { // encrypt headers - if (zquiet) verbosity_mod += " with encrypted headers"; + if (minisrv_config.config.debug_flags.quiet) verbosity_mod += " with encrypted headers"; var enc_headers = socket_sessions[socket.id].wtvsec.Encrypt(1, headers + end_of_line); - socket.write(new Uint8Array(concatArrayBuffer(enc_headers, data))); + sendToSocket(socket, new Buffer.from(concatArrayBuffer(enc_headers, data))); } else { - socket.write(new Uint8Array(concatArrayBuffer(Buffer.from(headers + end_of_line), data))); + sendToSocket(socket, new Buffer.from(concatArrayBuffer(Buffer.from(headers + end_of_line), data))); } - if (zquiet) console.log(" * Sent" + verbosity_mod + " " + headers_obj.http_response + " to client (Content-Type:", headers_obj['Content-Type'], "~", headers_obj['Content-length'], "bytes)"); + if (minisrv_config.config.debug_flags.quiet) console.log(" * Sent" + verbosity_mod + " " + headers_obj.http_response + " to client (Content-Type:", headers_obj['Content-Type'], "~", headers_obj['Content-length'], "bytes)"); } +} - if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data; - if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer; - if (socket_sessions[socket.id].secure_buffer) delete socket_sessions[socket.id].secure_buffer; - if (socket_sessions[socket.id].buffer) delete socket_sessions[socket.id].buffer; - if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers; - if (socket_sessions[socket.id].post_data) delete socket_sessions[socket.id].post_data; - if (socket_sessions[socket.id].post_data_length) delete socket_sessions[socket.id].post_data_length; - if (socket_sessions[socket.id].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown; +async function sendToSocket(socket, data) { + // buffer size = lesser of minisrv_config.config.chunk_size or size remaining + var chunk_size = 16384; + var can_write = true; + var expected_data_out = 0; + while ((socket.bytesWritten == 0 || socket.bytesWritten != expected_data_out) && can_write) { + if (expected_data_out === 0) expected_data_out = data.byteLength + (socket_sessions[socket.id].socket_total_written || 0); + if (socket.bytesWritten == expected_data_out) break; - if (socket_sessions[socket.id].close_me) socket.end(); - if (headers_obj["Connection"]) { - if (headers_obj["Connection"].toLowerCase() == "close" && wtv_connection_close == "true") { - socket.destroy(); + var data_left = (expected_data_out - socket.bytesWritten); + var buffer_size = (data_left >= chunk_size) ? chunk_size : data_left; + var offset = (data.byteLength - data_left); + var chunk = new Buffer.alloc(buffer_size); + data.copy(chunk, 0, offset, (offset + buffer_size)); + can_write = socket.write(chunk); + if (!can_write) { + socket.once('drain', function () { + sendToSocket(socket, data); + }); + break; } } + if (socket.bytesWritten == expected_data_out) { + socket_sessions[socket.id].socket_total_written = socket.bytesWritten; + if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data; + if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer; + if (socket_sessions[socket.id].secure_buffer) delete socket_sessions[socket.id].secure_buffer; + if (socket_sessions[socket.id].buffer) delete socket_sessions[socket.id].buffer; + if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers; + if (socket_sessions[socket.id].post_data) delete socket_sessions[socket.id].post_data; + if (socket_sessions[socket.id].post_data_length) delete socket_sessions[socket.id].post_data_length; + if (socket_sessions[socket.id].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown; + socket.setTimeout(minisrv_config.config.socket_timeout * 1000); + if (socket_sessions[socket.id].close_me) socket.end(); + if (socket_sessions[socket.id].destroy_me) socket.destroy(); + } } function concatArrayBuffer(buffer1, buffer2) { @@ -922,6 +804,89 @@ function moveObjectElement(currentKey, afterKey, obj) { if (next !== -1) return result; else return obj; } +function checkSecurity(socket) { + var out = null; + var ip2long = function (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; + }; + + var isInSubnet = function (ip, subnet) { + var mask, base_ip, long_ip = ip2long(ip); + if ((mask = subnet.match(/^(.*?)\/(\d{1,2})$/)) && ((base_ip = ip2long(mask[1])) >= 0)) { + var freedom = Math.pow(2, 32 - parseInt(mask[2])); + return (long_ip > base_ip) && (long_ip < base_ip + freedom - 1); + } + else return false; + }; + + var rejectSSIDConnection = function (ssid, blacklist) { + if (blacklist) console.log(" * Request from SSID", wtvshared.filterSSID(ssid), "(" + socket.remoteAddr + "), but that SSID is in the blacklist, rejecting."); + else console.log(" * Request from SSID", wtvshared.filterSSID(socket.ssid), "(" + socket.remoteAddress + "), but that SSID is not in the whitelist, rejecting."); + + var errpage = doErrorPage(401, "Access to this service is denied."); + out = errpage; + } + + var checkSSIDIPWhitelist = function (ssid, blacklist) { + var ssid_access_list_ip_override = false; + if (minisrv_config.config.ssid_ip_allow_list) { + if (minisrv_config.config.ssid_ip_allow_list[socket.ssid]) { + Object.keys(minisrv_config.config.ssid_ip_allow_list[socket.ssid]).forEach(function (k) { + if (minisrv_config.config.ssid_ip_allow_list[socket.ssid][k].indexOf('/') > 0) { + if (isInSubnet(socket.remoteAddress, minisrv_config.config.ssid_ip_allow_list[socket.ssid][k])) { + // remoteAddr is in allowed subnet + ssid_access_list_ip_override = true; + } + } else { + if (socket.remoteAddress == minisrv_config.config.ssid_ip_allow_list[socket.ssid][k]) { + // remoteAddr directly matches IP + ssid_access_list_ip_override = true; + } + } + }); + if (!ssid_access_list_ip_override) rejectSSIDConnection(socket.ssid, blacklist); + } else { + rejectSSIDConnection(socket.ssid, blacklist); + } + } else { + rejectSSIDConnection(socket.ssid, blacklist); + } + if (ssid_access_list_ip_override && minisrv_config.config.debug_flags.debug) console.log(" * Request from disallowed SSID", wtvshared.filterSSID(ssid), "was allowed due to IP address whitelist"); + } + + // process whitelist first + if (socket.ssid && minisrv_config.config.ssid_allow_list) { + var ssid_is_in_whitelist = minisrv_config.config.ssid_allow_list.findIndex(element => element == socket.ssid); + if (ssid_is_in_whitelist == -1) { + // no whitelist match, but lets see if the remoteAddress is allowed + checkSSIDIPWhitelist(socket.ssid, false); + } + } + + // now check blacklist + if (socket.ssid && minisrv_config.config.ssid_block_list) { + var ssid_is_in_blacklist = minisrv_config.config.ssid_block_list.findIndex(element => element == socket.ssid); + if (ssid_is_in_blacklist != -1) { + // blacklist match, but lets see if the remoteAddress is allowed + checkSSIDIPWhitelist(socket.ssid, true); + } + } + + // Passed Security + return out; +} + function isUnencryptedString(string, verbose = false) { // a generic "isAscii" check is not sufficient, as the test will see the binary // compressed / encrypted data as ASCII. This function checks for characters expected @@ -931,11 +896,6 @@ function isUnencryptedString(string, verbose = false) { return /^([A-Za-z0-9\+\/\=\-\.\,\ \"\;\:\?\&\r\n\(\)\%\<\>\_\~\*\@\#\\]{8,})$/.test(string); } -function filterSSID(ssid) { - var WTVCSD = new WTVClientSessionData(null,minisrv_config.config.hide_ssid_in_logs); - return WTVCSD.filterSSID(ssid); -} - async function processRequest(socket, data_hex, skipSecure = false, encryptedRequest = false) { // This function sucks and needs to be rewritten @@ -971,8 +931,8 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq // its not a POST and it failed the isUnencryptedString test, so we think this is an encrypted blob if (socket_sessions[socket.id].secure != true) { // first time so reroll sessions - if (zdebug) console.log(" # [ UNEXPECTED BINARY BLOCK ] First sign of encryption, re-creating RC4 sessions for socket id", socket.id); - socket_sessions[socket.id].wtvsec = new WTVSec(1, zdebug); + if (minisrv_config.config.debug_flags.debug) console.log(" # [ UNEXPECTED BINARY BLOCK ] First sign of encryption, re-creating RC4 sessions for socket id", socket.id); + socket_sessions[socket.id].wtvsec = new WTVSec(minisrv_config); socket_sessions[socket.id].wtvsec.IssueChallenge(); socket_sessions[socket.id].wtvsec.SecureOn(); socket_sessions[socket.id].secure = true; @@ -1003,10 +963,10 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq if (!headers) return; if (headers["wtv-client-serial-number"] != null && socket.ssid == null) { - socket.ssid = makeSafeSSID(headers["wtv-client-serial-number"]); + socket.ssid = wtvshared.makeSafeSSID(headers["wtv-client-serial-number"]); if (socket.ssid != null) { if (!ssid_sessions[socket.ssid]) { - ssid_sessions[socket.ssid] = new WTVClientSessionData(socket.ssid,minisrv_config.config.hide_ssid_in_logs); + ssid_sessions[socket.ssid] = new WTVClientSessionData(minisrv_config, socket.ssid); ssid_sessions[socket.ssid].SaveIfRegistered(); } if (!ssid_sessions[socket.ssid].data_store.sockets) ssid_sessions[socket.ssid].data_store.sockets = new Set(); @@ -1015,96 +975,21 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } } - var ip2long = function (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; - }; - - var isInSubnet = function (ip, subnet) { - var mask, base_ip, long_ip = ip2long(ip); - if ((mask = subnet.match(/^(.*?)\/(\d{1,2})$/)) && ((base_ip = ip2long(mask[1])) >= 0)) { - var freedom = Math.pow(2, 32 - parseInt(mask[2])); - return (long_ip > base_ip) && (long_ip < base_ip + freedom - 1); - } - else return false; - }; - - var rejectSSIDConnection = function (ssid, blacklist) { - if (blacklist) console.log(" * Request from SSID", filterSSID(ssid), "(" + socket.remoteAddr + "), but that SSID is in the blacklist, rejecting."); - else console.log(" * Request from SSID", filterSSID(socket.ssid), "(" + socket.remoteAddress + "), but that SSID is not in the whitelist, rejecting."); - - var errpage = doErrorPage(401, "Access to this service is denied."); - headers = errpage[0]; - data = errpage[1]; + var failed_security = checkSecurity(socket); + if (failed_security) { socket_sessions[socket.id].close_me = true; + headers = failed_security[0]; + data = failed_security[1]; } - var checkSSIDIPWhitelist = function (ssid, blacklist) { - var ssid_access_list_ip_override = false; - if (minisrv_config.config.ssid_ip_allow_list) { - if (minisrv_config.config.ssid_ip_allow_list[socket.ssid]) { - Object.keys(minisrv_config.config.ssid_ip_allow_list[socket.ssid]).forEach(function (k) { - if (minisrv_config.config.ssid_ip_allow_list[socket.ssid][k].indexOf('/') > 0) { - if (isInSubnet(socket.remoteAddress, minisrv_config.config.ssid_ip_allow_list[socket.ssid][k])) { - // remoteAddr is in allowed subnet - ssid_access_list_ip_override = true; - } - } else { - if (socket.remoteAddress == minisrv_config.config.ssid_ip_allow_list[socket.ssid][k]) { - // remoteAddr directly matches IP - ssid_access_list_ip_override = true; - } - } - }); - if (!ssid_access_list_ip_override) rejectSSIDConnection(socket.ssid, blacklist); - } else { - rejectSSIDConnection(socket.ssid, blacklist); - } - } else { - rejectSSIDConnection(socket.ssid, blacklist); - } - if (ssid_access_list_ip_override && zdebug) console.log(" * Request from disallowed SSID", filterSSID(ssid), "was allowed due to IP address whitelist"); - } - - // process whitelist first - if (socket.ssid && minisrv_config.config.ssid_allow_list) { - var ssid_is_in_whitelist = minisrv_config.config.ssid_allow_list.findIndex(element => element == socket.ssid); - if (ssid_is_in_whitelist == -1) { - // no whitelist match, but lets see if the remoteAddress is allowed - checkSSIDIPWhitelist(socket.ssid, false); - } - } - - // now check blacklist - if (socket.ssid && minisrv_config.config.ssid_block_list) { - var ssid_is_in_blacklist = minisrv_config.config.ssid_block_list.findIndex(element => element == socket.ssid); - if (ssid_is_in_blacklist != -1) { - // blacklist match, but lets see if the remoteAddress is allowed - checkSSIDIPWhitelist(socket.ssid, true); - } - } - - // Passed Security - if (headers["wtv-capability-flags"] != null) { if (!ssid_sessions[socket.ssid]) { - ssid_sessions[socket.ssid] = new WTVClientSessionData(socket.ssid,minisrv_config.config.hide_ssid_in_logs); + ssid_sessions[socket.ssid] = new WTVClientSessionData(minisrv_config, socket.ssid); ssid_sessions[socket.ssid].SaveIfRegistered(); } if (!ssid_sessions[socket.ssid].capabilities) ssid_sessions[socket.ssid].capabilities = new WTVClientCapabilities(headers["wtv-capability-flags"]); } - // log all client wtv- headers to the SessionData for that SSID // this way we can pull up client info such as wtv-client-rom-type or wtv-system-sysconfig if (socket.ssid) { @@ -1121,14 +1006,14 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq if (ssid_sessions[socket.ssid]) { if (headers["wtv-ticket"]) { if (!ssid_sessions[socket.ssid].data_store.wtvsec_login) { - ssid_sessions[socket.ssid].data_store.wtvsec_login = new WTVSec(); + ssid_sessions[socket.ssid].data_store.wtvsec_login = new WTVSec(minisrv_config); ssid_sessions[socket.ssid].data_store.wtvsec_login.IssueChallenge(); ssid_sessions[socket.ssid].data_store.wtvsec_login.set_incarnation(headers["wtv-incarnation"]); ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64 = headers["wtv-ticket"]; ssid_sessions[socket.ssid].data_store.wtvsec_login.DecodeTicket(ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64); } else { if (ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64 != headers["wtv-ticket"]) { - if (zdebug) console.log(" # New ticket from client"); + if (minisrv_config.config.debug_flags.debug) console.log(" # New ticket from client"); ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64 = headers["wtv-ticket"]; ssid_sessions[socket.ssid].data_store.wtvsec_login.DecodeTicket(ssid_sessions[socket.ssid].data_store.wtvsec_login.ticket_b64); ssid_sessions[socket.ssid].data_store.wtvsec_login.set_incarnation(headers["wtv-incarnation"]); @@ -1140,11 +1025,11 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq if ((headers.secure === true || headers.encrypted === true) && !skipSecure) { if (!socket_sessions[socket.id].wtvsec) { - if (!zquiet) console.log(" * Starting new WTVSec instance on socket", socket.id); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Starting new WTVSec instance on socket", socket.id); if (ssid_sessions[socket.ssid].get("wtv-incarnation")) { - socket_sessions[socket.id].wtvsec = new WTVSec(ssid_sessions[socket.ssid].get("wtv-incarnation"), zdebug); + socket_sessions[socket.id].wtvsec = new WTVSec(minisrv_config, ssid_sessions[socket.ssid].get("wtv-incarnation")); } else { - socket_sessions[socket.id].wtvsec = new WTVSec(1, zdebug); + socket_sessions[socket.id].wtvsec = new WTVSec(minisrv_config); } socket_sessions[socket.id].wtvsec.DecodeTicket(headers["wtv-ticket"]); socket_sessions[socket.id].wtvsec.ticket_b64 = headers["wtv-ticket"]; @@ -1152,7 +1037,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } if (socket_sessions[socket.id].secure != true) { // first time so reroll sessions - if (zdebug) console.log(" # [ SECURE ON BLOCK (" + socket.id + ") ]"); + if (minisrv_config.config.debug_flags.debug) console.log(" # [ SECURE ON BLOCK (" + socket.id + ") ]"); socket_sessions[socket.id].secure = true; } if (!headers.request_url) { @@ -1166,46 +1051,37 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } var enc_data = CryptoJS.enc.Hex.parse(data_hex.substring(header_length * 2)); if (enc_data.sigBytes > 0) { - if (isUnencryptedString(enc_data.toString(CryptoJS.enc.Latin1), (!skipSecure && !encryptedRequest))) { - // some builds (like our targeted 3833), send SECURE ON but then unencrypted headers - if (zdebug) console.log(" # Psuedo-encrypted Request (SECURE ON)", "on", socket.id); - // don't actually encrypt output - headers.psuedo_encryption = true; - ssid_sessions[socket.ssid].set("box-does-psuedo-encryption", true); - socket_sessions[socket.id].secure = false; - var secure_headers = await processRequest(socket, enc_data.toString(CryptoJS.enc.Hex), true, true); - } else { - // SECURE ON and detected encrypted data - ssid_sessions[socket.ssid].set("box-does-psuedo-encryption", false); - var dec_data = CryptoJS.lib.WordArray.create(socket_sessions[socket.id].wtvsec.Decrypt(0, enc_data)) - if (!socket_sessions[socket.id].secure_buffer) socket_sessions[socket.id].secure_buffer = ""; - socket_sessions[socket.id].secure_buffer += dec_data.toString(CryptoJS.enc.Hex); - var secure_headers = null; - if (headers['request']) { - if (headers['request'] == "GET") { - if (socket_sessions[socket.id].secure_buffer.indexOf("0d0a0d0a") || socket_sessions[socket.id].secure_buffer.indexOf("0a0a")) { - secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); - } - } else { - secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); + + // SECURE ON and detected encrypted data + var dec_data = CryptoJS.lib.WordArray.create(socket_sessions[socket.id].wtvsec.Decrypt(0, enc_data)) + if (!socket_sessions[socket.id].secure_buffer) socket_sessions[socket.id].secure_buffer = ""; + socket_sessions[socket.id].secure_buffer += dec_data.toString(CryptoJS.enc.Hex); + var secure_headers = null; + if (headers['request']) { + if (headers['request'] == "GET") { + if (socket_sessions[socket.id].secure_buffer.indexOf("0d0a0d0a") || socket_sessions[socket.id].secure_buffer.indexOf("0a0a")) { + secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); } } else { - secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); - } - if (!secure_headers) return; - - delete socket_sessions[socket.id].secure_buffer; - if (zdebug) console.log(" # Encrypted Request (SECURE ON)", "on", socket.id); - if (zshowheaders) console.log(secure_headers); - if (!secure_headers.request) { - socket_sessions[socket.id].secure = false; - var errpage = doErrorPage(400); - headers = errpage[0]; - data = errpage[1]; - sendToClient(socket, headers, data); - return; + secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); } + } else { + secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); } + if (!secure_headers) return; + + delete socket_sessions[socket.id].secure_buffer; + if (minisrv_config.config.debug_flags.debug) console.log(" # Encrypted Request (SECURE ON)", "on", socket.id); + if (minisrv_config.config.debug_flags.show_headers) console.log(secure_headers); + if (!secure_headers.request) { + socket_sessions[socket.id].secure = false; + var errpage = doErrorPage(400); + headers = errpage[0]; + data = errpage[1]; + sendToClient(socket, headers, data); + return; + } + // Merge new headers into existing headers object Object.keys(secure_headers).forEach(function (k) { headers[k] = secure_headers[k]; @@ -1232,8 +1108,9 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } // handle POST - if (headers['request']) { + if (headers['request'] && !socket_sessions[socket.id].expecting_post_data) { if (headers['request'].substring(0, 4) == "POST") { + socket.setTimeout(minisrv_config.config.post_data_socket_timeout * 1000); if (typeof socket_sessions[socket.id].post_data == "undefined") { if (socket_sessions[socket.id].post_data_percents_shown) delete socket_sessions[socket.id].post_data_percents_shown; socket_sessions[socket.id].post_data_length = headers['Content-length'] || headers['Content-Length'] || 0; @@ -1241,35 +1118,36 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq socket_sessions[socket.id].post_data = ""; socket_sessions[socket.id].headers = headers; var post_string = "POST"; - if (socket_sessions[socket.id].secure == true) { - post_string = "Encrypted " + post_string; - } + if (socket_sessions[socket.id].secure) post_string = "Encrypted " + post_string; // the client may have just sent the data with the primary headers, so lets look for that. if (data_hex.indexOf("0d0a0d0a") != -1) socket_sessions[socket.id].post_data = data_hex.substring(data_hex.indexOf("0d0a0d0a") + 8); if (data_hex.indexOf("0a0a") != -1) socket_sessions[socket.id].post_data = data_hex.substring(data_hex.indexOf("0a0a") + 4); - } + if (socket_sessions[socket.id].post_data.length == (socket_sessions[socket.id].post_data_length * 2)) { // got all expected data if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data; - console.log(" * Incoming", post_string, "request on", socket.id, "from", filterSSID(socket.ssid), "to", headers['request_url'], "(got all expected", socket_sessions[socket.id].post_data_length, "bytes of data from client already)"); + console.log(" * Incoming", post_string, "request on", socket.id, "from", wtvshared.filterSSID(socket.ssid), "to", headers['request_url'], "(got all expected", socket_sessions[socket.id].post_data_length, "bytes of data from client already)"); headers.post_data = CryptoJS.enc.Hex.parse(socket_sessions[socket.id].post_data); - if (socket_sessions[socket.id].headers) delete socket_sessions[socket.id].headers; + delete socket_sessions[socket.id].headers; + delete socket_sessions[socket.id].post_data; + delete socket_sessions[socket.id].post_data_length; processURL(socket, headers); - } else { - // expecting more data (see below) - socket_sessions[socket.id].expecting_post_data = true; - console.log(" * Incoming", post_string, "request on", socket.id, "from", filterSSID(socket.ssid), "to", headers['request_url'], "(expecting", socket_sessions[socket.id].post_data_length, "bytes of data from client...)"); - } - if (socket_sessions[socket.id].post_data.length > (socket_sessions[socket.id].post_data_length * 2)) { - // got too much data ? ... should not ever reach this code - var errpage = doErrorPage(400, "Received too much data in POST request
    Got " + (socket_sessions[socket.id].post_data.length / 2) + ", expected " + socket_sessions[socket.id].post_data_length); + } else if (socket_sessions[socket.id].post_data.length > (socket_sessions[socket.id].post_data_length * 2)) { + // got too much data ? ... should not ever reach this code (section 2) + var errpage = doErrorPage(400, "Received too much data in POST request
    Got " + (socket_sessions[socket.id].post_data.length / 2) + ", expected " + socket_sessions[socket.id].post_data_length) + " (2)"; headers = errpage[0]; data = errpage[1]; sendToClient(socket, headers, data); return; - } + } else { + // expecting more data (see below) + socket_sessions[socket.id].expecting_post_data = true; + if (!socket_sessions[socket.id].post_data) socket_sessions[socket.id].post_data = ''; + socket_sessions[socket.id].post_data += CryptoJS.enc.Hex.parse(socket_sessions[socket.id].post_data); + console.log(" * Incoming", post_string, "request on", socket.id, "from", wtvshared.filterSSID(socket.ssid), "to", headers['request_url'], "(expecting", socket_sessions[socket.id].post_data_length, "bytes of data from client...)"); + } return; } else { delete socket_sessions[socket.id].headers; @@ -1306,15 +1184,12 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq console.log(" * ", Math.floor(new Date().getTime() / 1000), "Receiving", post_string, "data on", socket.id, "[", socket_sessions[socket.id].post_data.length / 2, "of", socket_sessions[socket.id].post_data_length, "bytes ]"); } else { // calculate and display percentage of data received - var getPercentage = function (partialValue, totalValue) { - return Math.floor((100 * partialValue) / totalValue); - } - var postPercent = getPercentage(socket_sessions[socket.id].post_data.length, (socket_sessions[socket.id].post_data_length * 2)); + var postPercent = wtvshared.getPercentage(socket_sessions[socket.id].post_data.length, (socket_sessions[socket.id].post_data_length * 2)); if (minisrv_config.config.post_percentages) { if (minisrv_config.config.post_percentages.includes(postPercent)) { if (!socket_sessions[socket.id].post_data_percents_shown) socket_sessions[socket.id].post_data_percents_shown = new Array(); if (!socket_sessions[socket.id].post_data_percents_shown[postPercent]) { - console.log(" * Received", postPercent, "% of", socket_sessions[socket.id].post_data_length, "bytes on", socket.id, "from", filterSSID(socket.ssid)); + console.log(" * Received", postPercent, "% of", socket_sessions[socket.id].post_data_length, "bytes on", socket.id, "from", wtvshared.filterSSID(socket.ssid)); socket_sessions[socket.id].post_data_percents_shown[postPercent] = true; } if (postPercent == 100) delete socket_sessions[socket.id].post_data_percents_shown; @@ -1325,20 +1200,21 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq if (socket_sessions[socket.id].post_data.length == (socket_sessions[socket.id].post_data_length * 2)) { // got all expected data if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data; + socket.setTimeout(minisrv_config.config.socket_timeout * 1000); headers.post_data = CryptoJS.enc.Hex.parse(socket_sessions[socket.id].post_data); if (socket_sessions[socket.id].secure == true) { - if (zdebug) console.log(" # Encrypted POST Content (SECURE ON)", "on", socket.id, "[", headers.post_data.sigBytes, "bytes ]"); + if (minisrv_config.config.debug_flags.debug) console.log(" # Encrypted POST Content (SECURE ON)", "on", socket.id, "[", headers.post_data.sigBytes, "bytes ]"); } else { - if (zdebug) console.log(" # Unencrypted POST Content", "on", socket.id); + if (minisrv_config.config.debug_flags.debug) console.log(" # Unencrypted POST Content", "on", socket.id); } delete socket_sessions[socket.id].headers; delete socket_sessions[socket.id].post_data; delete socket_sessions[socket.id].post_data_length; processURL(socket, headers); return; - } - if (socket_sessions[socket.id].post_data.length > (socket_sessions[socket.id].post_data_length * 2)) { + } else if (socket_sessions[socket.id].post_data.length > (socket_sessions[socket.id].post_data_length * 2)) { if (socket_sessions[socket.id].expecting_post_data) delete socket_sessions[socket.id].expecting_post_data; + socket.setTimeout(minisrv_config.config.socket_timeout * 1000); // got too much data ? ... should not ever reach this code var errpage = doErrorPage(400, "Received too much data in POST request
    Got " + (socket_sessions[socket.id].post_data.length / 2) + ", expected " + socket_sessions[socket.id].post_data_length); headers = errpage[0]; @@ -1350,7 +1226,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } else if (!skipSecure) { if (!encryptedRequest) { if (socket_sessions[socket.id].secure != true) { - socket_sessions[socket.id].wtvsec = new WTVSec(1, zdebug); + socket_sessions[socket.id].wtvsec = new WTVSec(minisrv_config); socket_sessions[socket.id].wtvsec.IssueChallenge(); socket_sessions[socket.id].wtvsec.SecureOn(); socket_sessions[socket.id].secure = true; @@ -1414,7 +1290,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq async function cleanupSocket(socket) { try { if (socket_sessions[socket.id]) { - if (!zquiet) console.log(" * Cleaning up disconnected socket", socket.id); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * Cleaning up disconnected socket", socket.id); delete socket_sessions[socket.id]; } if (socket.ssid) { @@ -1434,7 +1310,7 @@ async function cleanupSocket(socket) { // set timeout to check ssid_sessions[socket.ssid].data_store.socket_check = setTimeout(function (ssid) { if (ssid_sessions[ssid].currentConnections() === 0) { - if (!zquiet) console.log(" * WebTV SSID", filterSSID(ssid), " has not been seen in", (timeout / 1000), "seconds, cleaning up session data for this SSID"); + if (!minisrv_config.config.debug_flags.quiet) console.log(" * WebTV SSID", wtvshared.filterSSID(ssid), " has not been seen in", (timeout / 1000), "seconds, cleaning up session data for this SSID"); delete ssid_sessions[ssid]; } }, timeout, socket.ssid); @@ -1453,21 +1329,25 @@ async function handleSocket(socket) { socket_sessions[socket.id] = []; socket.minisrv_pc_mode = false; socket.setEncoding('hex'); //set data encoding (Text: 'ascii', 'utf8' ~ Binary: 'hex', 'base64' (do not trust 'binary' encoding)) - socket.setTimeout(10800000); // 3 hours + socket.setTimeout(minisrv_config.config.socket_timeout * 1000); socket.on('data', function (data_hex) { - if (!socket_sessions[socket.id].secure && !socket_sessions[socket.id].expecting_post_data) { - // buffer unencrypted data until we see the classic double-newline, or get blank - if (!socket_sessions[socket.id].header_buffer) socket_sessions[socket.id].header_buffer = ""; - socket_sessions[socket.id].header_buffer += data_hex; - if (socket_sessions[socket.id].header_buffer.indexOf("0d0a0d0a") != -1 || socket_sessions[socket.id].header_buffer.indexOf("0a0a") != -1) { - data_hex = socket_sessions[socket.id].header_buffer; - delete socket_sessions[socket.id].header_buffer; + if (socket_sessions[socket.id]) { + if (!socket_sessions[socket.id].secure && !socket_sessions[socket.id].expecting_post_data) { + // buffer unencrypted data until we see the classic double-newline, or get blank + if (!socket_sessions[socket.id].header_buffer) socket_sessions[socket.id].header_buffer = ""; + socket_sessions[socket.id].header_buffer += data_hex; + if (socket_sessions[socket.id].header_buffer.indexOf("0d0a0d0a") != -1 || socket_sessions[socket.id].header_buffer.indexOf("0a0a") != -1) { + data_hex = socket_sessions[socket.id].header_buffer; + delete socket_sessions[socket.id].header_buffer; + processRequest(this, data_hex); + } + } else { + // stream encrypted requests through the processor + if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer; processRequest(this, data_hex); } } else { - // stream encrypted requests through the processor - if (socket_sessions[socket.id].header_buffer) delete socket_sessions[socket.id].header_buffer; - processRequest(this, data_hex); + cleanupSocket(socket); } }); @@ -1563,7 +1443,7 @@ try { } } } catch (e) { - if (zdebug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration."); + if (minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration."); } if (throw_me) { @@ -1645,40 +1525,41 @@ process.on('uncaughtException', function (err) { }); // defaults -var zdebug = false; -var zquiet = true; // will squash zdebug even if its true -var zshowheaders = false; +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; if (minisrv_config.config.verbosity) { switch (minisrv_config.config.verbosity) { case 0: - zdebug = false; - zquiet = true; - zshowheaders = false; + minisrv_config.config.debug_flags.debug = false; + minisrv_config.config.debug_flags.quiet = true; + minisrv_config.config.debug_flags.show_headers = false; console.log(" * Console Verbosity level 0 (quietest)") break; case 1: - zdebug = false; - zquiet = true; - zshowheaders = true; + minisrv_config.config.debug_flags.debug = false; + minisrv_config.config.debug_flags.quiet = true; + minisrv_config.config.debug_flags.show_headers = true; console.log(" * Console Verbosity level 1 (headers shown)") break; case 2: - zdebug = true; - zquiet = true; - zshowheaders = false; + minisrv_config.config.debug_flags.debug = true; + minisrv_config.config.debug_flags.quiet = true; + minisrv_config.config.debug_flags.show_headers = false; console.log(" * Console Verbosity level 2 (verbose without headers)") break; case 3: - zdebug = true; - zquiet = true; - zshowheaders = true; + minisrv_config.config.debug_flags.debug = true; + minisrv_config.config.debug_flags.quiet = true; + minisrv_config.config.debug_flags.show_headers = true; console.log(" * Console Verbosity level 3 (verbose with headers)") break; default: - zdebug = true; - zquiet = false; - zshowheaders = true; + minisrv_config.config.debug_flags.debug = true; + minisrv_config.config.debug_flags.quiet = false; + minisrv_config.config.debug_flags.show_headers = true; console.log(" * Console Verbosity level 4 (debug verbosity)") break; } @@ -1702,6 +1583,8 @@ bind_ports.forEach(function (v) { initstring = initstring.substring(0, initstring.length - 2); +const wtvshared = new WTVShared(minisrv_config); +const wtvmime = new WTVMime(minisrv_config); console.log(" * Started server on ports " + initstring + "...") var listening_ip_string = (minisrv_config.config.bind_ip != "0.0.0.0") ? "IP: " + minisrv_config.config.bind_ip : "all interfaces"; diff --git a/zefie_wtvp_minisrv/config.json b/zefie_wtvp_minisrv/config.json index 80bd772d..db14be70 100644 --- a/zefie_wtvp_minisrv/config.json +++ b/zefie_wtvp_minisrv/config.json @@ -13,12 +13,15 @@ "hide_ssid_in_logs": true, "post_percentages": [ 0, 25, 50, 100 ], "verbosity": 2, + "socket_timeout": 86400, + "post_data_socket_timeout": 30, "error_log_file": "errors.log", "catchall_file_name": "catchall.js", "enable_lzpf_compression": false, "enable_gzip_compression": true, "pc_server_hidden_service": "http_pc", "pc_server_hidden_service_enabled": false, + "show_diskmap": false, "allow_guests": true }, "services": { diff --git a/zefie_wtvp_minisrv/package.json b/zefie_wtvp_minisrv/package.json index f16d9473..3486c8d5 100644 --- a/zefie_wtvp_minisrv/package.json +++ b/zefie_wtvp_minisrv/package.json @@ -1,6 +1,6 @@ { "name": "zefie_wtvp_minisrv", - "version": "0.9.16", + "version": "0.9.18", "description": "WebTV Service (WTVP) Emulation Server", "main": "app.js", "homepage": "https://github.com/zefie/zefie_wtvp_minisrv", diff --git a/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj b/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj index 17205247..29397b0b 100644 --- a/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj +++ b/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj @@ -52,6 +52,10 @@ Code + + + Code + Code @@ -275,12 +279,18 @@ Code + + Code + Code Code + + Code + Code @@ -288,6 +298,9 @@ Code + + Code +