diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js b/zefie_wtvp_minisrv/ServiceVault/wtv-1800/preregister.js index 1c063b1e..8548ff59 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,11 +20,11 @@ } } }); - if (i > 0 && zdebug) console.log(" # Closed", i, "previous sockets for", filterSSID(socket.ssid)); + if (i > 0 && zdebug) 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 (zdebug) console.log(" # Recreating primary WTVSec login instance for", wtvshared.filterSSID(socket.ssid)); delete ssid_sessions[socket.ssid].data_store.wtvsec_login; } diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js index 24ec9e75..52bd0ceb 100644 --- a/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js +++ b/zefie_wtvp_minisrv/ServiceVault/wtv-disk/sync.js @@ -1,10 +1,8 @@ -// todo: async +const WTVDownloadList = require("./WTVDownloadList.js"); +var wtvdl = new WTVDownloadList(service_name); var force_update = (request_headers.query.force == "true") ? true : false; if (request_headers['wtv-request-type'] == 'download') { - const WTVDownloadList = require("./WTVDownloadList.js"); - var wtvdl = new WTVDownloadList(); - var content_dir = "content/" var diskmap_dir = content_dir + "diskmaps/"; @@ -273,58 +271,6 @@ if (request_headers['wtv-request-type'] == 'download') { } 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\nContent-Type: text/html"; + data = wtvdl.getSyncPage(minisrv_config, 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..2ca7c442 --- /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) { + if (socket.ssid) { + if (ssid_sessions[socket.ssid]) { + if (ssid_sessions[socket.ssid].isRegistered()) { + var result = ssid_sessions[socket.ssid].storeUserStoreFile(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-head-waiter/login-stage-two.js b/zefie_wtvp_minisrv/ServiceVault/wtv-head-waiter/login-stage-two.js index aaff9e62..40281814 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 @@ -16,11 +16,11 @@ 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)); + console.log(" * wtv-challenge-response FAILED for " + wtvshared.filterSSID(socket.ssid)); if (zdebug) console.log("Response Expected:", challenge_response.toString(CryptoJS.enc.Base64)); if (zdebug) console.log("Response Received:", client_challenge_response) gourl = "wtv-head-waiter:/login?reissue_challenge=true"; diff --git a/zefie_wtvp_minisrv/ServiceVault/wtv-log/log.js b/zefie_wtvp_minisrv/ServiceVault/wtv-log/log.js index 47fa7299..84c43d47 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 (!zquiet) 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 (!zquiet) 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-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/WTVClientSessionData.js b/zefie_wtvp_minisrv/WTVClientSessionData.js index d8bd9e70..bb54e363 100644 --- a/zefie_wtvp_minisrv/WTVClientSessionData.js +++ b/zefie_wtvp_minisrv/WTVClientSessionData.js @@ -4,6 +4,7 @@ class WTVClientSessionData { fs = require('fs'); path = require('path'); + ssid = null; data_store = null; session_store = null; @@ -11,48 +12,107 @@ class WTVClientSessionData { capabilities = null; session_storage = ""; hide_ssid_in_logs = true; - - 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; - } - } + wtvshared = null; + wtvmime = null; constructor(ssid, hide_ssid_in_logs, session_storage_directory) { + var { WTVShared, clientShowAlert } = require('./WTVShared.js'); + var WTVMimeTypes = require('./WTVMimeTypes.js'); + this.wtvshared = new WTVShared(); + this.wtvmime = new WTVMimeTypes(); + 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"; + if (!session_storage_directory) session_storage_directory = __dirname + this.path.sep + "SessionStore"; this.session_storage = session_storage_directory; this.data_store = new Array(); this.session_store = {}; } + /** + * Returns the absolute path to the user's file store + */ + getUserStoreDirectory() { + return this.session_storage + 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 result = false; + var store_dir = this.getUserStoreDirectory(); + 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(); + 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 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 the number of user cookies + * @returns {number} Number of cookies + */ countCookies() { return Object.keys(this.session_store.cookies).length || 0; } @@ -184,7 +244,7 @@ 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; } } @@ -209,7 +269,7 @@ class WTVClientSessionData { 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"); 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; } } @@ -249,7 +309,7 @@ class WTVClientSessionData { } } 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 index d685ca9d..688b7c1c 100644 --- a/zefie_wtvp_minisrv/WTVDownloadList.js +++ b/zefie_wtvp_minisrv/WTVDownloadList.js @@ -3,10 +3,22 @@ * By: zefie */ class WTVDownloadList { - download_list = ""; - content_type = "wtv/download-list"; - constructor() { + download_list = ""; + service_name = ""; + content_type = "wtv/download-list"; + wtvshared = null; + clientShowAlert = null; + + /** + * Constructs the WTVDownloadList Class + * @param {string} service_name Service name to use in wtv-urls + */ + constructor(service_name = "wtv-disk") { + var { WTVShared, clientShowAlert } = require('./WTVShared.js'); + this.wtvshared = new WTVShared(); + this.clientShowAlert = clientShowAlert; + this.service_name = service_name this.clear(); } @@ -98,13 +110,31 @@ class WTVDownloadList { * 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 - * @param {string} display Message to display while working on this file */ put(path, destination) { this.download_list += "PUT " + path + "\n"; this.download_list += "location: " + destination + "\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.download_list += "PUT " + path + "\n"; + this.download_list += "location: " + this.service_name + ":/userstore?partialPath="+escape(destination) + "\n"; + } + + /** + * Alias to putUserStoreDest() that generates the destination + * @param {any} path + */ + putUserStore(path) { + var destination = path.replace("file://", ""); + this.download_list += "PUT " + path + "\n"; + this.download_list += "location: " + this.service_name + ":/userstore?partialPath=" + escape(destination) + "\n"; + } /** * Adds a GET command to the download list * @param {string} file Non-absolute path of client destination file (relative to group base) @@ -169,6 +199,99 @@ class WTVDownloadList { 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(minisrv_config, 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': 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': minisrv_config.config.service_logo, + 'message': "Download failed...", + 'buttonlabel1': "Okay...", + '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/WTVMimeTypes.js b/zefie_wtvp_minisrv/WTVMimeTypes.js new file mode 100644 index 00000000..a0024c01 --- /dev/null +++ b/zefie_wtvp_minisrv/WTVMimeTypes.js @@ -0,0 +1,133 @@ +/** + * Simple class for WebTV Mime Types and overrides + */ + + +class WTVMimeTypes { + + mime = require('mime-types'); + wtvshared = null; + + + constructor() { + var { WTVShared, clientShowAlert } = require('./WTVShared.js'); + this.wtvshared = new WTVShared(); + if (!String.prototype.reverse) { + String.prototype.reverse = function () { + var splitString = this.split(""); + var reverseArray = splitString.reverse(); + var joinArray = reverseArray.join(""); + return joinArray; + } + } + } + + /** + * 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 = WTVMimeTypes; diff --git a/zefie_wtvp_minisrv/WTVShared.js b/zefie_wtvp_minisrv/WTVShared.js new file mode 100644 index 00000000..5da2faff --- /dev/null +++ b/zefie_wtvp_minisrv/WTVShared.js @@ -0,0 +1,141 @@ +/** + * Shared functions across all classes and apps + */ + +class WTVShared { + + path = require('path'); + + constructor() { + if (!String.prototype.reverse) { + String.prototype.reverse = function () { + var splitString = this.split(""); + var reverseArray = splitString.reverse(); + var joinArray = reverseArray.join(""); + return joinArray; + } + } + } + + /** + * Returns a censored SSID + * @param {string|Array} obj SSID String or Headers Object + */ + 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; + } + } + + /** + * 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; + } + + /** + * 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 039445ae..bd796ae4 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -8,13 +8,17 @@ 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 WTVMimeTypes = require("./WTVMimeTypes.js"); +var { WTVShared, clientShowAlert } = require("./WTVShared.js"); + +const wtvshared = new WTVShared(); +const wtvmime = new WTVMimeTypes(); process .on('SIGTERM', shutdown('SIGTERM')) @@ -70,10 +74,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 +105,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 +113,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("/"); @@ -231,7 +138,7 @@ async function processPath(socket, service_vault_file_path, request_headers = ne 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); + 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]; @@ -345,47 +252,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(); @@ -469,7 +335,7 @@ 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; } @@ -481,14 +347,14 @@ async function processURL(socket, request_headers) { reqverb = "Psuedo-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 (zshowheaders) console.log(" * Incoming headers on socket ID", socket.id, (await wtvshared.filterSSID(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 +371,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 (zshowheaders) 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; @@ -841,7 +707,7 @@ async function sendToClient(socket, headers_obj, data) { } // header object to string - if (zshowheaders) console.log(" * Outgoing headers on socket ID", socket.id, (await filterSSID(headers_obj))); + if (zshowheaders) 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; @@ -930,11 +796,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 @@ -1002,7 +863,7 @@ 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); @@ -1039,8 +900,8 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq }; 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."); + 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."); headers = errpage[0]; @@ -1072,7 +933,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq } 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"); + if (ssid_access_list_ip_override && zdebug) console.log(" * Request from disallowed SSID", wtvshared.filterSSID(ssid), "was allowed due to IP address whitelist"); } // process whitelist first @@ -1252,14 +1113,14 @@ 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; - 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; 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...)"); + 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...)"); } 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 @@ -1313,7 +1174,7 @@ async function processRequest(socket, data_hex, skipSecure = false, encryptedReq 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; @@ -1433,7 +1294,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 (!zquiet) 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);