diff --git a/README.md b/README.md index 5e88c245..c5e0ea95 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,9 @@ This open source server is in alpha status. Use at your own risk. - Test with a WebTV Viewer or connect with a real box - To connect with a real box, you will need to open ports in your firewall and have a way to connect your WebTV (and preferably reroute 10.0.0.1 to the server) - See [ServiceVault.md](ServiceVault.md) for a brief introduction to how the service files work + +### How to Support the Project +- [Report Bugs](https://github.com/zefie/zefie_wtvp_minisrv/issues) +- [Add a Feature and send a Pull Request](https://github.com/zefie/zefie_wtvp_minisrv/pulls) +- Write and submit better documentation than I created (see Pull Request above) +- [Support financially on Patreon](https://www.patreon.com/zefie) \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js index 2885e20f..ebb0d9a6 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js @@ -6,6 +6,27 @@ if (request_headers["wtv-ticket"]) { gourl = "wtv-head-waiter:/login-stage-two?"; } +if (socket.ssid) { + if (ssid_sessions[socket.ssid].data_store) { + if (ssid_sessions[socket.ssid].data_store.sockets) { + var i = 0; + ssid_sessions[socket.ssid].data_store.sockets.forEach(function (k) { + if (typeof k != "undefined") { + if (k != socket) { + k.destroy(); + ssid_sessions[socket.ssid].data_store.sockets.delete(k); + i++; + } + } + }); + if (i > 0 && zdebug) console.log(" # Closed", i, "previous sockets for", socket.ssid); + } + } + if (ssid_sessions[socket.ssid].data_store.wtvsec_login) { + delete ssid_sessions[socket.ssid].data_store.wtvsec_login; + } +} + headers = `200 OK Connection: Keep-Alive wtv-expire-all: wtv- diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-chat/home.html b/zefie_wtvp_minisrv/ServiceVault/wtv-chat/home.html new file mode 100644 index 00000000..2f14e323 --- /dev/null +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-chat/home.html @@ -0,0 +1,123 @@ + + + +Chat Home + + + + + + + +
+ + + +
+ +
+ + + + + +
+ + + + +
+
+ + + + + + + +
+
+
+
+ + + + + + + +
+ +
+ +
+ + + + +
+ +
+
+ + +
+ + +
   + + + + +
+ + +
+Chat Home +
+ + +
+
+
+ + + + + + + +
+
+ +yo yo yo yo + +enter your nick... + + +
+ + + + +
+
+ + +
+
+ + + +
+ + + + + + + +
+
+ + diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-chat/images/htv_chat.gif b/zefie_wtvp_minisrv/ServiceVault/wtv-chat/images/htv_chat.gif new file mode 100644 index 00000000..7b19a7d9 Binary files /dev/null and b/zefie_wtvp_minisrv/ServiceVault/wtv-chat/images/htv_chat.gif differ diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-chat/images/htv_chat.jpg b/zefie_wtvp_minisrv/ServiceVault/wtv-chat/images/htv_chat.jpg new file mode 100644 index 00000000..3b66cc7c Binary files /dev/null and b/zefie_wtvp_minisrv/ServiceVault/wtv-chat/images/htv_chat.jpg differ 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 8ce70907..5f4e23a1 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 @@ -96,7 +96,10 @@ wtv-demo-mode: 0 wtv-wink-deferrer-retries: 3 wtv-offline-mail-enable: false wtv-name-server: 8.8.8.8 -wtv-settings-url: wtv-setup:/get -wtv-visit: wtv-home:/splash? +`; + if (ssid_sessions[socket.ssid].get('wtv-need-upgrade') != 'true') { + headers += "wtv-settings-url: wtv-setup:/get\n"; + } + headers += `wtv-visit: wtv-home:/splash? Content-Type: text/html`; } \ No newline at end of file diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js b/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js index 3f8be789..0b59c112 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-home/home.js @@ -28,14 +28,13 @@ function go() {

- +
@@ -35,7 +35,7 @@ WebTV Music Index
- +
@@ -48,7 +48,7 @@ WebTV Music Index
- +
@@ -74,7 +74,7 @@ WebTV Music Index
- +
@@ -87,7 +87,7 @@ WebTV Music Index
- +
diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/images/About_bg.jpg b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/images/About_bg.jpg new file mode 100644 index 00000000..dbfb1d9e Binary files /dev/null and b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/images/About_bg.jpg differ diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js index e69de29b..9afbc6b1 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-tricks/info.js @@ -0,0 +1,148 @@ + +var client_caps = null; + +if (socket.ssid != null) { + if (ssid_sessions[socket.ssid].data_store.capabilities) { + client_caps = ssid_sessions[socket.ssid].data_store.capabilities; + } +} +if (client_caps) { + headers = `200 OK +Content-Type: text/html` + + + var client_label = "TODO"; + var boot_client_label = "TODO"; + var wtv_system_sysconfig_str = "TODO"; + + + 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_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); + + var capabilities_table = new WTVClientCapabilities().capabilities_table; + + + + data = ` + + +${minisrv_config.config.service_name} Info + + + + + + +
+
+
+ +

${minisrv_config.config.service_name} Info

+ +
+ + + + + + + + + + + + + + +
+
Connected to: + + Mini Service +
Service: + + ${z_title} +
Client: + + &vers; (Build ${wtv_system_version} [${client_label}]) +
Boot: + + &wtv-bootvers; (Build ${wtv_client_bootrom_version} [${boot_client_label}]) +
+ Silicon serial ID: + + ${wtv_client_serial_number} +
Connected at: + + &rate; +
Client IP number: + + ${socket.remoteAddress} +
+
ROM type: + + ${wtv_client_rom_type} +
Modem f/w (when available): + + &modem; +`; + if (ssid_sessions[socket.ssid].get("wtv-need-upgrade")) { + data += `
Mini-browser: + + Yes +`; + } + data += ` +
Chip version: + + ${wtv_system_chipversion_str} (TODO) +
SysConfig: + + 0x${wtv_system_sysconfig_hex.toUpperCase()} +
+ + + + +
+
Client capabilities: + + +
+ +`; + + + // start loop + + Object.keys(capabilities_table).forEach(function (k) { + data += ` +
${capabilities_table[k][1]} + + `; + if (client_caps[capabilities_table[k][0]]) data += "True\n"; + else data += "False\n"; + }); + +// end loop + +data += ` +
+ +
+
+
+${wtv_system_sysconfig_str}
+
+
+ + `; +} else { + var errpage = doErrorPage(400); + headers = errpage[0]; + data = errpage[1]; +} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/WTVClientCapabilities.js b/zefie_wtvp_minisrv/WTVClientCapabilities.js new file mode 100644 index 00000000..587586cb --- /dev/null +++ b/zefie_wtvp_minisrv/WTVClientCapabilities.js @@ -0,0 +1,150 @@ +class WTVClientCapabilities { + + /***********************************\ + |* Special Thanks to: *| + |* Outatyme *| + |* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *| + |* For the binary information *| + |* about capability flags *| + \***********************************/ + + capabilities = null; + capabilities_table = null; + + + constructor(wtv_capability_flags = null) { + // [ flag_name, friendly_flag_name ] + // so far we assume the reversed bit order = the order on wtv-tricks:/info (production service) + // also speculation that `client-has-relogin-function` is forced true on the service side + // (this script does not do that, also note that LC2 MiniBrowser does not support client:relog) + // None of this is 100% for certain yet (except the bitfield stuff), do not trust as verbatim, more testing needed + + var capabilities_table = [ + ["client-can-do-muzac", "Can Do Muzac"], + ["client-can-do-chat", "Can Chat"], + ["client-can-do-openISP", "Can do OpenISP"], + ["client-can-receive-compressed-data", "Can receive compressed data"], + ["client-can-display-spotads1", "Can show Spotads1"], + ["client-can-print", "Can Print"], + ["client-can-do-macromedia-flash1", "Can do Macromedia Flash1"], + ["client-can-do-javascript", "Can do JavaScript"], + ["client-can-do-videoflash", "Can do VideoFlash"], + ["client-can-do-videoads", "Can do VideoAds"], + ["client-has-disk", "Has Disk"], + ["client-supports-classical-service", "Supports Classical"], + ["client-open-isp-settings-valid", "OISP settings valid"], + ["client-can-tell-valid-open-isp", "Can tell OISP settings valid"], + ["client-has-tuner", "Has Tuner"], + ["client-can-data-download", "Can data download"], + ["client-supports-approx-content-len", "Supports approximate content length"], + ["client-has-built-in-printer-port", "Has built-in printer port"], + ["client-has-tv-experience", "Has TV experience"], + ["client-can-handle-proxy-bypass", "Can handle proxy bypass"], + ["client-can-handle-download-v2", "Can handle Download protocol 2"], + ["client-has-relogin-function", "Has Relogin function"], + ["client-can-display-spotads2", "Can display spotads2"], + ["client-can-display-30-sec-video-ads", "Can display 30 second video ads"], + ["client-supports-etude-service", "Supports Etude"], + ["client-can-do-av-capture", "Can do AV capture"], + ["client-can-do-disconnected-email", "Can do disconnected email"], + ["client-can-do-macromedia-flash2", "Can do Macromedia Flash2"], + ["client-has-memory-size-bit1-set", "Memory size bit1 set"], + ["client-has-memory-size-bit2-set", "Memory size bit2 set"], + ["client-has-memory-size-bit3-set", "Memory size bit3 set"], + ["client-can-do-rmf", "Can do RMF"], + ["client-can-do-png", "Can do PNG"], + ["client-does-broadband-data-dowload", "Supports broadband download"], + ["client-has-softmodem", "Has Softmodem"], + ["client-can-do-preparsed-epg", "Can do pre-parsed EPG"], + ["client-supports-funk-e-service", "Supports Funk-e"], + ["client-wants-dial-script", "Wants dial script"], + ["client-upgrade-visits-not-needed", "Upgrade visits not needed"], + ["client-uses-flexible-videoad-paths", "Uses flexible videoad paths"], + ["client-non-production-build", "Non-production build"], + ["client-can-download-printer-drivers", "Can download printer drivers"], + ["client-supports-hiphop-service", "Supports HipHop"], + ["client-can-use-messenger", "Can use MSN Messenger"], + ["client-uses-third-party-billing", "Uses 3rd-party billing"], + ["client-can-do-offlineads", "Can do offline ads"], + ["client-has-no-dialin-support", "Has no dialin support"], + ["client-has-ssl-support-for-wtvp", "Has SSL support for WTVP"], + ["client-can-do-audio-capture", "Can do audio capture"], + ["client-can-do-metered-pricing", "Can do Metered Pricing"], + ["client-negotiates-user-agent", "Can Negotiate User-Agent"], + ["client-can-do-element-logging", "Can do Unsupported Element Logging"], + ["client-supports-jazz-security", "Supports Jazz security"], + ["client-supports-MSN-service", "Supports MSN service"], + ["client-supports-notify-port-header", "Supports notify port header"], + ["client-supports-messenger-update-light", "Supports MSN Messenger update light"], + ["client-supports-MSN-chat", "Supports MSN Chat"], + ["client-supports-MSN-chat-findu", "Supports MSN Chat FindU"], + ["client-supports-MSN-messenger-CVR", "Supports MSN Messenger CVR"], + ["client-supports-MSN-messenger-MSNP8", "Supports MSN Messenger MSNP8"], + ["client-supports-MSN-chat-R9C", "Supports MSN Chat R9C"] + ]; + + this.capabilities_table = capabilities_table; + + var capabilities = new Array(); + + // might want to pass without a flag to get the table + if (wtv_capability_flags != null) { + + // define function to convert hex string to binary string (0s & 1s) + var hex2bin = function (hex) { + var binary = ""; + var remainingSize = hex.length; + for (var p = 0; p < hex.length / 8; p++) { + //In case remaining hex length (or initial) is not multiple of 8 + var blockSize = remainingSize < 8 ? remainingSize : 8; + + binary += parseInt(hex.substr(p * 8, blockSize), 16).toString(2); + + remainingSize -= blockSize; + } + return binary; + } + + // Add .reverse() to strings for ease of processing + if (!String.prototype.reverse) { + String.prototype.reverse = function () { + var splitString = this.split(""); + var reverseArray = splitString.reverse(); + var joinArray = reverseArray.join(""); + return joinArray; + } + } + + // convert wtv_capability_flags to binary string, reverse the string, and split into array containing each character; + var bitfield = hex2bin(wtv_capability_flags).reverse().split(""); + + // only add to the capabilities array if the result is true + var add = function (flag_name, flag) { + if (flag) capabilities[flag_name] = flag; + } + + // process bitfield and set capabilities + Object.keys(bitfield).forEach(function (k) { + // Convert binary to boolean, 0 to false, 1 to true + var bitfield_result = (bitfield[k] == "1") + + // set flags based on position of bit + add(capabilities_table[k][0], bitfield_result); + }); + + this.capabilities = capabilities; + return capabilities; + } + } + + get(key = null) { + if (typeof (this.capabilities) === 'undefined') return null; + else if (key === null) return this.capabilities; + else if (this.capabilities[key]) return this.capabilities[key]; + else return null; + } + +} + + +module.exports = WTVClientCapabilities; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/session_data.js b/zefie_wtvp_minisrv/WTVClientSessionData.js similarity index 66% rename from zefie_wtvp_minisrv/session_data.js rename to zefie_wtvp_minisrv/WTVClientSessionData.js index f6e14679..7bec66c7 100644 --- a/zefie_wtvp_minisrv/session_data.js +++ b/zefie_wtvp_minisrv/WTVClientSessionData.js @@ -1,4 +1,13 @@ -class ClientSessionData { +class WTVClientSessionData { + + /***********************************\ + |* Special Thanks to: *| + |* No one *| + |* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *| + |* There is literally nothing *| + |* special about this class *| + \***********************************/ + data_store = null; constructor() { @@ -25,4 +34,4 @@ class ClientSessionData { } -module.exports = ClientSessionData; \ No newline at end of file +module.exports = WTVClientSessionData; \ No newline at end of file diff --git a/zefie_wtvp_minisrv/wtvsec.js b/zefie_wtvp_minisrv/WTVSec.js similarity index 95% rename from zefie_wtvp_minisrv/wtvsec.js rename to zefie_wtvp_minisrv/WTVSec.js index 147bc03d..99206228 100644 --- a/zefie_wtvp_minisrv/wtvsec.js +++ b/zefie_wtvp_minisrv/WTVSec.js @@ -2,9 +2,21 @@ const CryptoJS = require('crypto-js'); const endianness = require('endianness'); var crypto = require('crypto'); +/***********************************\ +|* Special Thanks to: *| +|* eMac (Eric MacDonald) *| +|* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ *| +|* For the encryption/decryption *| +|* information and process *| +\***********************************/ + class WTVSec { - //initial_shared_key = CryptoJS.lib.WordArray.random(8); - initial_shared_key_b64 = "CC5rWmRUE0o="; // You can change this but it doesn't mean much for security. Just make sure its static. 8 bytes base64 encoded. + // Initial Shared Key, in Base64 Format + // You can change this but it doesn't mean much for security. Just make sure its static. 8 bytes base64 encoded. + // If you intend to link multiple minisrv's together, they must all share the same Initial Shared Key. + + initial_shared_key_b64 = "CC5rWmRUE0o="; + initial_shared_key = null; current_shared_key = null; challenge_key = null; diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index 366abe2d..f0e09753 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -1,6 +1,7 @@ 'use strict'; const fs = require('fs'); +const path = require('path'); const http = require('http'); const https = require('https'); const strftime = require('strftime'); // used externally by service scripts @@ -8,8 +9,9 @@ const net = require('net'); const CryptoJS = require('crypto-js'); const mime = require('mime-types'); const { crc16 } = require('easy-crc'); -var WTVSec = require('./wtvsec.js'); -var ClientSessionData = require('./session_data.js'); +var WTVSec = require('./WTVSec.js'); +var WTVClientCapabilities = require('./WTVClientCapabilities.js'); +var WTVClientSessionData = require('./WTVClientSessionData.js'); // Where we store our session information var ssid_sessions = new Array(); @@ -19,11 +21,13 @@ var ports = []; // add .reverse() feature to all JavaScript Strings in this application // works for service vault scripts too. -String.prototype.reverse = function () { - var splitString = this.split(""); - var reverseArray = splitString.reverse(); - var joinArray = reverseArray.join(""); - return joinArray; +if (!String.prototype.reverse) { + String.prototype.reverse = function () { + var splitString = this.split(""); + var reverseArray = splitString.reverse(); + var joinArray = reverseArray.join(""); + return joinArray; + } } function getServiceString(service) { @@ -89,7 +93,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 = service_vault_dir + "/" + service_path.replace(/\\/g, "/"); + service_vault_file_path = makeSafePath(service_vault_dir,service_path); if (fs.existsSync(service_vault_file_path)) { @@ -139,7 +143,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne service_vault_found = true; if (!zquiet) console.log(" * Found " + service_vault_file_path + ".js to handle request (JS Interpreter mode) [Socket " + socket.id + "]"); // expose var service_dir for script path to the root of the wtv-service - var service_dir = service_vault_dir.replace(/\\/g, "/") + "/" + service_name; + 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); @@ -166,7 +170,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne } if (!request_is_async) { if (!service_vault_found) { - console.log(" * Could not find a Service Vault for", service_path); + console.log(" * Could not find a Service Vault for " + service_name + ":/" + service_path.replace(service_name + path.sep, "")); var errpage = doErrorPage(404); headers = errpage[0]; data = errpage[1]; @@ -209,6 +213,12 @@ function filterSSID(obj) { } } +function makeSafePath(base, target) { + target.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, ""); + var targetPath = path.posix.normalize(target) + return base + path.sep + targetPath; +} + async function processURL(socket, request_headers) { if (request_headers === null) { return; @@ -251,7 +261,7 @@ async function processURL(socket, request_headers) { } // assume webtv since there is a :/ in the GET var service_name = shortURL.split(':/')[0]; - var urlToPath = service_name + "/" + shortURL.split(':/')[1]; + var urlToPath = service_name + path.sep + shortURL.split(':/')[1]; if (zshowheaders) console.log(" * Incoming headers on socket ID", socket.id, (await filterSSID(request_headers))); processPath(socket, urlToPath, request_headers, service_name); } else if (shortURL.indexOf('http://') >= 0 || shortURL.indexOf('https://') >= 0) { @@ -502,7 +512,16 @@ async function sendToClient(socket, headers_obj, 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)"); } - socket_sessions[socket.id].buffer = null; + + 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; + if (socket_sessions[socket.id].close_me) socket.end(); if (headers_obj["Connection"]) { if (headers_obj["Connection"].toLowerCase() == "close" && wtv_connection_close == "true") { @@ -550,11 +569,11 @@ function headersAreStandard(string, verbose = false) { return /^([A-Za-z0-9\+\/\=\-\.\,\ \"\;\:\?\&\r\n\(\)\%\<\>\_]{8,})$/.test(string); } -async function processRequest(socket, data_hex, returnHeadersBeforeSecure = false, encryptedRequest = false) { +async function processRequest(socket, data_hex, skipSecure = false, encryptedRequest = false) { - // TODO: clean up this function (how much is even used anymore?) + // This function sucks and needs to be rewritten - var headers = null; + var headers = new Array(); if (socket_sessions[socket.id]) { if (socket_sessions[socket.id].headers) { headers = socket_sessions[socket.id].headers; @@ -570,7 +589,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals data = data.split("\n\n")[0]; } if (headersAreStandard(data)) { - if (headers != null) { + if (headers.length != 0) { var new_header_obj = headerStringToObj(data); Object.keys(new_header_obj).forEach(function (k, v) { headers[k] = new_header_obj[k]; @@ -579,7 +598,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } else { headers = headerStringToObj(data); } - } else if (!returnHeadersBeforeSecure) { + } else if (!skipSecure) { // if its a POST request, assume its a binary blob and not encrypted (dangerous) if (!encryptedRequest) { // its not a POST and it failed the headersAreStandard test, so we think this is an encrypted blob @@ -614,13 +633,15 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } } + if (!headers) return; + if (headers["wtv-client-serial-number"] != null) { socket.ssid = headers["wtv-client-serial-number"]; if (!ssid_sessions[socket.ssid]) { - ssid_sessions[socket.ssid] = new ClientSessionData(); + ssid_sessions[socket.ssid] = new WTVClientSessionData(); } - if (!ssid_sessions[socket.ssid].data_store.sockets) ssid_sessions[socket.ssid].data_store.sockets = new Array(); - ssid_sessions[socket.ssid].data_store.sockets.push(socket.id); + if (!ssid_sessions[socket.ssid].data_store.sockets) ssid_sessions[socket.ssid].data_store.sockets = new Set(); + ssid_sessions[socket.ssid].data_store.sockets.add(socket); } var ip2long = function (ip) { @@ -702,6 +723,15 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } } + // Passed Security + + if (headers["wtv-capability-flags"] != null) { + if (!ssid_sessions[socket.ssid]) { + ssid_sessions[socket.ssid] = new WTVClientSessionData(); + } + if (!ssid_sessions[socket.ssid].data_store.capabilities) ssid_sessions[socket.ssid].data_store.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 @@ -734,11 +764,8 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } } - if (returnHeadersBeforeSecure) { - return headers; - } - if (headers.secure === true || headers.encrypted === true) { + 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 (ssid_sessions[socket.ssid].get("wtv-incarnation")) { @@ -766,7 +793,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } var enc_data = CryptoJS.enc.Hex.parse(data_hex.substring(header_length * 2)); if (enc_data.sigBytes > 0) { - if (headersAreStandard(enc_data.toString(CryptoJS.enc.Latin1), (!returnHeadersBeforeSecure && !encryptedRequest))) { + if (headersAreStandard(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 @@ -778,8 +805,23 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals // 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)) - var secure_headers = await processRequest(socket, dec_data.toString(CryptoJS.enc.Hex), true, true); + 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); + } + } 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) { @@ -792,11 +834,28 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } } // Merge new headers into existing headers object - Object.keys(secure_headers).forEach(function (k, v) { + Object.keys(secure_headers).forEach(function (k) { headers[k] = secure_headers[k]; }); + } else { + socket_sessions[socket.id].headers = headers; + return; } } + } else if (skipSecure) { + if (headers) { + if (headers['request']) { + if (headers['request'].substring(0, 4) == "POST") { + if (socket_sessions[socket.id].secure_buffer) delete socket_sessions[socket.id].secure_buffer; + } else { + return headers; + } + } else { + return headers; + } + } else { + return; + } } // handle POST @@ -818,11 +877,14 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } 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)"); 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; 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)) { @@ -841,7 +903,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals delete socket_sessions[socket.id].post_data_length; processURL(socket, headers); return; - } + } } else { socket_sessions[socket.id].headers = headers; } @@ -870,7 +932,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals 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) { + 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)); @@ -888,6 +950,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } 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; 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 ]"); @@ -901,6 +964,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals return; } 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; // 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]; @@ -909,7 +973,7 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals return; } - } else if (!returnHeadersBeforeSecure) { + } else if (!skipSecure) { if (!encryptedRequest) { if (socket_sessions[socket.id].secure != true) { socket_sessions[socket.id].wtvsec = new WTVSec(1, zdebug); @@ -933,23 +997,41 @@ async function processRequest(socket, data_hex, returnHeadersBeforeSecure = fals } else { var dec_data = CryptoJS.lib.WordArray.create(socket_sessions[socket.id].wtvsec.Decrypt(0, enc_data)); } - var secure_headers = await processRequest(socket, dec_data.toString(CryptoJS.enc.Hex), false, true); + 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 { + var secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); + } + } else { + var secure_headers = await processRequest(socket, socket_sessions[socket.id].secure_buffer, true, true); + } if (secure_headers) { + delete socket_sessions[socket.id].secure_buffer; if (!headers) headers = new Array(); headers.encrypted = true; Object.keys(secure_headers).forEach(function (k, v) { headers[k] = secure_headers[k]; }); if (headers['request']) { - if (headers['request'].substring(0, 4) == "POST" && !socket_sessions[socket.id].post_data) { - socket_sessions[socket.id].post_data_length = headers['Content-length'] || headers['Content-Length'] || 0; - socket_sessions[socket.id].post_data = ""; + if (headers['request'].substring(0, 4) == "POST") { + if (!socket_sessions[socket.id].post_data) { + socket_sessions[socket.id].post_data_length = headers['Content-length'] || headers['Content-Length'] || 0; + socket_sessions[socket.id].post_data = ""; + } + processRequest(socket, dec_data.toString(CryptoJS.enc.Hex)); + } else { + processURL(socket, headers); } - processRequest(socket, dec_data.toString(CryptoJS.enc.Hex)); } - } + } } - } + } } } } @@ -962,12 +1044,9 @@ async function cleanupSocket(socket) { delete socket_sessions[socket.id]; } if (socket.ssid) { - var socket_array_index = ssid_sessions[socket.ssid].data_store.sockets.findIndex(element => element == socket.id); - if (socket_array_index != -1) { - ssid_sessions[socket.ssid].data_store.sockets.splice(socket_array_index,1); - } + ssid_sessions[socket.ssid].data_store.sockets.delete(socket); - if (ssid_sessions[socket.ssid].data_store.sockets.length === 0 && ssid_sessions[socket.ssid].data_store.wtvsec_login) { + if (ssid_sessions[socket.ssid].data_store.sockets.size === 0 && ssid_sessions[socket.ssid].data_store.wtvsec_login) { // if last socket for SSID disconnected, destroy login session if (!zquiet) console.log(" * Last socket from WebTV SSID", filterSSID(socket.ssid),"disconnected, cleaning up primary WTVSec instance for this SSID"); ssid_sessions[socket.ssid].delete("wtvsec_login"); @@ -989,20 +1068,20 @@ async function handleSocket(socket) { socket.id = parseInt(crc16('CCITT-FALSE', Buffer.from(String(socket.remoteAddress) + String(socket.remotePort), "utf8")).toString(16), 16); socket_sessions[socket.id] = []; socket.setEncoding('hex'); //set data encoding (Text: 'ascii', 'utf8' ~ Binary: 'hex', 'base64' (do not trust 'binary' encoding)) - + socket.setTimeout(600000); socket.on('data', function (data_hex) { - if (!socket_sessions[socket.id].secure) { + 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; - var header_buffer_text = CryptoJS.enc.Hex.parse(socket_sessions[socket.id].header_buffer).toString(CryptoJS.enc.Latin1); - if (header_buffer_text.indexOf("\r\n\r\n") != -1 || header_buffer_text.indexOf("\n\n") != -1 || header_buffer_text == "") { + 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); } }); @@ -1041,25 +1120,24 @@ function integrateConfig(main, user) { return main; } -function returnAbsolsutePath(path) { - if (path.substring(0, 1) != "/" && path.substring(1, 1) != ":") { +function returnAbsolsutePath(check_path) { + if (check_path.substring(0, 1) != path.sep && check_path.substring(1, 1) != ":") { // non-absolute path, so use current directory as base - path = (__dirname + "/" + path).replace(/\\/g, "/"); + check_path = (__dirname + path.sep + check_path); } else { // already absolute path - path = path.replace(/\\/g, "/"); } - return path; + return check_path; } function getGitRevision() { try { - const rev = fs.readFileSync(__dirname.replace(/\\/g, "/") + '/../.git/HEAD').toString().trim(); + const rev = fs.readFileSync(__dirname + path.sep + ".." + path.sep + ".git" + path.sep + "HEAD").toString().trim(); if (rev.indexOf(':') === -1) { return rev; } else { - return fs.readFileSync(__dirname.replace(/\\/g, "/") + '/../.git/' + rev.substring(5)).toString().trim(); + return fs.readFileSync(__dirname + path.sep + ".." + path.sep + ".git" + path.sep + rev.substring(5)).toString().trim(); } } catch (e) { return null; @@ -1214,4 +1292,3 @@ initstring = initstring.substring(0, initstring.length - 2); 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"; console.log(" * Listening on", listening_ip_string,"~","Service IP:", service_ip); - diff --git a/zefie_wtvp_minisrv/package.json b/zefie_wtvp_minisrv/package.json index 14ecdc61..5286c4aa 100644 --- a/zefie_wtvp_minisrv/package.json +++ b/zefie_wtvp_minisrv/package.json @@ -1,10 +1,13 @@ { "name": "zefie_wtvp_minisrv", - "version": "0.9.5", + "version": "0.9.6", "description": "WebTV Service (WTVP) Emulation Server", "main": "app.js", "homepage": "https://github.com/zefie/zefie_wtvp_minisrv", "license": "GPL3", + "scripts": { + "test": "node test.js" + }, "author": { "name": "zefie", "email": "zefie@zefie.net", diff --git a/zefie_wtvp_minisrv/test.js b/zefie_wtvp_minisrv/test.js new file mode 100644 index 00000000..49c0d926 --- /dev/null +++ b/zefie_wtvp_minisrv/test.js @@ -0,0 +1,33 @@ +const { promisify } = require('util'); +const { resolve } = require('path'); +const fs = require('fs'); +const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat); +const { exec } = require("child_process"); +var path = require('path'); + +async function getFiles(dir) { + const subdirs = await readdir(dir); + const files = await Promise.all(subdirs.map(async (subdir) => { + const res = resolve(dir, subdir); + return (await stat(res)).isDirectory() ? getFiles(res) : res; + })); + return files.reduce((a, f) => a.concat(f), []); +} + +getFiles(__dirname) + .then(files => { + files.forEach(function (file) { + if (path.extname(file) == ".js" && file.indexOf("node_modules") == -1) { + console.log(" * Checking syntax of", file.replace(__dirname + path.sep, "." + path.sep)); + exec("node --check \"" + file + "\"", (error, stdout, stderr) => { + if (stderr.length > 0) { + console.log(`${stderr}`); + return; + } + }); + } + }); + }) + .catch(e => console.error(e)); + diff --git a/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj b/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj index 02be7160..2c248647 100644 --- a/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj +++ b/zefie_wtvp_minisrv/zefie_wtvp_minisrv.njsproj @@ -202,12 +202,16 @@ - + + Code - + + Code + + Code