move classes and base config into includes directory

This commit is contained in:
zefie
2022-10-11 10:20:25 -04:00
parent eeac2621e3
commit 4589d7a4e8
40 changed files with 130 additions and 101 deletions

View File

@@ -0,0 +1,172 @@
class WTVAdmin {
fs = require('fs');
path = require('path');
minisrv_config = [];
wtvr = null;
wtvshared = null;
wtvclient = null;
WTVClientSessionData = require("./WTVClientSessionData.js");
service_name = "wtv-admin";
constructor(minisrv_config, wtvclient, service_name) {
this.minisrv_config = minisrv_config;
var { WTVShared } = require("./WTVShared.js");
var WTVRegister = require("./WTVRegister.js");
this.wtvclient = wtvclient;
this.wtvshared = new WTVShared(minisrv_config);
this.wtvr = new WTVRegister(minisrv_config);
this.clientAddress = wtvclient.getClientAddress();
this.service_name = service_name;
}
ip2long(ip) {
var components;
if (components = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
var iplong = 0;
var power = 1;
for (var i = 4; i >= 1; i -= 1) {
iplong += power * parseInt(components[i]);
power *= 256;
}
return iplong;
}
else return -1;
}
isInSubnet(ip, subnet) {
if (subnet.indexOf('/') == -1) {
var mask, base_ip, long_ip = this.ip2long(ip);
var mask2, base_ip2, long_ip2 = this.ip2long(ip);
return (long_ip == long_ip2);
} else {
var mask, base_ip, long_ip = this.ip2long(ip);
if ((mask = subnet.match(/^(.*?)\/(\d{1,2})$/)) && ((base_ip = this.ip2long(mask[1])) >= 0)) {
var freedom = Math.pow(2, 32 - parseInt(mask[2]));
return (long_ip > base_ip) && (long_ip < base_ip + freedom - 1);
}
}
return false;
}
rejectConnection(reason_is_ssid) {
var rejectReason;
if (reason_is_ssid) {
rejectReason = this.wtvclient.ssid + " is not in the whitelist.";
console.log(" * Request from SSID", this.wtvshared.filterSSID(this.wtvclient.ssid), "(" + this.clientAddress + ") for wtv-admin, but that SSID is not in the admin whitelist.");
} else {
rejectReason = this.clientAddress + " is not in the whitelist for SSID " + this.wtvclient.ssid + ".";
console.log(" * Request from SSID", this.wtvshared.filterSSID(this.wtvclient.ssid), "(" + this.clientAddress + ") for wtv-admin, but that IP is not authorized for that SSID.");
}
return rejectReason;
}
checkPassword(password) {
if (this.minisrv_config.services[this.service_name].password) {
return (password == this.minisrv_config.services[this.service_name].password);
} else {
// no password set
return true;
}
}
isAuthorized() {
var allowed_ssid = false;
var allowed_ip = false;
if (this.minisrv_config.services[this.service_name].authorized_ssids) {
var self = this;
Object.keys(self.minisrv_config.services[this.service_name].authorized_ssids).forEach(function (k) {
if (typeof self.minisrv_config.services[self.service_name].authorized_ssids[k] == "string") {
var ssid = self.minisrv_config.services[self.service_name].authorized_ssids[k]
if (ssid == self.wtvclient.ssid) allowed_ssid = true;
allowed_ip = true; // no ip block defined
} else {
var ssid = k;
if (ssid == self.wtvclient.ssid) {
allowed_ssid = true;
Object.keys(self.minisrv_config.services[self.service_name].authorized_ssids[k]).forEach(function (j) {
if (self.isInSubnet(self.clientAddress, self.minisrv_config.services[self.service_name].authorized_ssids[k][j])) {
allowed_ip = true;
}
});
}
}
});
}
return (allowed_ssid && allowed_ip) ? true : this.rejectConnection(!allowed_ssid);
}
getAccountInfo(username, directory = null) {
var search_dir = this.minisrv_config.config.SessionStore;
var account_data = null;
var self = this;
if (directory) search_dir = directory;
this.fs.readdirSync(search_dir).forEach(file => {
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && account_data === null) {
account_data = self.getAccountInfo(username, search_dir + self.path.sep + file);
}
if (account_data !== null) return;
if (!file.match(/.*\.json/ig)) return;
try {
var temp_session_data_file = self.fs.readFileSync(search_dir + self.path.sep + file, 'Utf8');
var temp_session_data = JSON.parse(temp_session_data_file);
if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) {
account_data = [temp_session_data, (search_dir + self.path.sep + file).replace(this.minisrv_config.config.SessionStore + this.path.sep, "").split(this.path.sep)[0]];
}
} catch (e) {
console.error(" # Error parsing Session Data JSON", search_dir + self.path.sep + file, e);
}
});
if (account_data !== null) {
if (account_data.ssid) return account_data;
var account_info = {};
account_info.ssid = account_data[1];
account_info.username = account_data[0].subscriber_username;
account_info.user_id = account_data[0].subscriber_userid;
var userSession = new this.WTVClientSessionData(this.minisrv_config, account_info.ssid);
userSession.user_id = 0;
account_info.account_users = userSession.listPrimaryAccountUsers();
return account_info;
}
return null;
}
getAccountInfoBySSID(ssid) {
var account_info = {};
var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid);
userSession.user_id = 0;
if (userSession.isRegistered(false)) {
account_info.ssid = ssid;
account_info.account_users = userSession.listPrimaryAccountUsers();
account_info.username = account_info.account_users['subscriber'].subscriber_username;
account_info.user_id = 0;
return account_info;
}
else return false;
}
getAccountBySSID(ssid) {
var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid);
userSession.user_id = 0;
return userSession;
}
isBanned(ssid) {
var self = this;
var isBanned = false;
if (this.minisrv_config.config.ssid_block_list) {
Object.keys(this.minisrv_config.config.ssid_block_list).forEach(function (k) {
if (self.minisrv_config.config.ssid_block_list[k] == ssid) {
isBanned = true;
}
});
}
return isBanned;
}
}
module.exports = WTVAdmin;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,156 @@
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("");
var add = function (flag_name, flag) {
capabilities[flag_name] = flag;
}
var i = 0;
// 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
try {
add(capabilities_table[k][0], bitfield_result);
} catch (ex) {
add('unknown-capability-' + i, bitfield_result);
i++;
//console.error(" * Unknown configuration bit", k, "with value", 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;

View File

@@ -0,0 +1,852 @@
const { lib } = require('crypto-js');
const CryptoJS = require('crypto-js');
const WTVMail = require("./WTVMail.js")
const WTVSec = require("./WTVSec.js");
const WTVFavorites = require("./WTVFavorites.js");
class WTVClientSessionData {
fs = require('fs');
path = require('path');
ssid = null;
data_store = null;
session_store = null;
mailstore = null;
favstore = null;
login_security = null;
capabilities = null;
session_storage = "";
minisrv_config = [];
wtvshared = null;
wtvmime = null;
lockdown = null;
lockdownReason = null;
lockdownWhitelist = null;
baddisk = false;
clientAddress = null;
user_id = 0;
cryptoKey = "PNa$WN7gz}!T=t6X7^=|Ii##CEB~p\EP";
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);
this.lockdown = false;
this.ssid = ssid;
this.data_store = new Array();
this.session_store = {};
this.lockdownWhitelist = [
"wtv-1800:/preregister",
"wtv-head-waiter:/login",
"wtv-head-waiter:/ValidateLogin",
"wtv-head-waiter:/login-stage-two",
"wtv-head-waiter:/relogin",
"wtv-head-waiter:/ROMCache/Spacer.gif",
"wtv-head-waiter:/ROMCache/NameStrip.gif",
"wtv-head-waiter:/images/PasswordBanner.gif",
"wtv-head-waiter:/ROMCache/UtilityBullet.gif",
"wtv-head-waiter:/images/NameBanner.gif",
"wtv-head-waiter:/bad-disk",
"wtv-log:/log"
];
this.lockdownWhitelist.push(minisrv_config.config.unauthorized_url);
this.lockdownWhitelist.push(minisrv_config.config.service_logo);
this.mailstore = new WTVMail(this.minisrv_config, this)
this.favstore = new WTVFavorites(this.minisrv_config, this)
this.loginWhitelist = Object.assign([], this.lockdownWhitelist); // clone lockdown whitelist into login whitelist
this.loginWhitelist.push("wtv-head-waiter:/choose-user");
this.loginWhitelist.push("wtv-head-waiter:/password");
this.loginWhitelist.push("http://*"); // allow http proxy without login
this.loginWhitelist.push("https://*"); // allow https proxy without login
}
assignMailStore() {
this.mailstore = new WTVMail(this.minisrv_config, this)
}
createWTVSecSession() {
return new WTVSec(this.minisrv_config)
}
getAccountTotalUnreadMessages() {
if (!this.isRegistered()) return false; // unregistered
if (this.user_id > 0) return false; // not primary user or pre-login
var total_unread_messages = 0;
for (var i = 0; i < this.minisrv_config.config.user_accounts.max_users_per_account; i++) {
var subUserSession = new this.constructor(this.minisrv_config, this.ssid);
subUserSession.switchUserID(i, false, false);
subUserSession.assignMailStore();
if (subUserSession.mailstore) {
total_unread_messages += subUserSession.mailstore.countUnreadMessages(0);
}
}
return total_unread_messages;
}
switchUserID(user_id, update_mail = true, update_ticket = true) {
this.user_id = user_id;
this.loadSessionData();
if (this.isRegistered()) this.assignMailStore();
if (this.data_store.wtvsec_login && update_ticket) this.setTicketData('user_id', user_id);
}
setTicketData(key, value) {
if (this.data_store.wtvsec_login) this.data_store.wtvsec_login.setTicketData(key, value);
else return false;
return true;
}
getTicketData(key) {
if (this.data_store.wtvsec_login) return this.data_store.wtvsec_login.getTicketData(key);
return false;
}
deleteTicketData(key) {
if (this.data_store.wtvsec_login) this.data_store.wtvsec_login.deleteTicketData(key);
else return false;
return true;
}
findFreeUserSlot() {
if (this.user_id != 0) return false; // subscriber only command
var master_directory = this.getUserStoreDirectory(true);
if (this.fs.existsSync(master_directory)) {
for (var i = 0; i < this.minisrv_config.config.user_accounts.max_users_per_account; i++) {
var test_dir = master_directory + this.path.sep + "user" + i;
if (!this.fs.existsSync(test_dir)) {
return i;
break;
}
}
}
return false;
}
getDisplayName() {
return (this.user_id == 0) ? this.getSessionData("subscriber_name") : this.getSessionData("display_name");
}
getNumberOfUserAccounts() {
if (!this.isRegistered()) return false;
if (this.user_id != 0) return false; // subscriber only command
return Object.keys(this.listPrimaryAccountUsers()).length;
}
listPrimaryAccountUsers() {
if (this.user_id != 0) return false; // subscriber only command
var master_directory = this.getUserStoreDirectory(true);
var account_data = [];
var self = this;
this.fs.readdirSync(master_directory).forEach(f => {
if (self.fs.lstatSync(master_directory + self.path.sep + f).isDirectory()) {
if (f.substr(0, 4) == "user") {
var user_file = master_directory + self.path.sep + f + self.path.sep + f + ".json";
if (self.fs.existsSync(user_file)) {
if (f == "user0") account_data['subscriber'] = JSON.parse(this.fs.readFileSync(user_file));
else account_data[f] = JSON.parse(this.fs.readFileSync(user_file));
}
}
}
});
return account_data;
}
mkdirRecursive(thedir) {
thedir.split(this.path.sep).reduce(
(directories, directory) => {
directories += directory + this.path.sep;
if (!this.fs.existsSync(directories)) {
this.fs.mkdirSync(directories);
}
return directories;
},
'',
);
}
/**
* Returns the absolute path to the user's file store, or false if unregistered
* @param subscriber {boolean} Returns the parent subscriber directory instead of the user's directory
* @returns {string|boolean} Absolute path to the user's file store, or false if unregistered
*/
getUserStoreDirectory(subscriber = false, user_id = null) {
if (user_id == null) user_id = this.user_id;
var userstore = this.minisrv_config.config.SessionStore + this.path.sep + this.ssid + this.path.sep;
if (!subscriber) userstore += "user" + user_id + this.path.sep;
return userstore;
}
removeUser(user_id) {
if (!this.isRegistered()) return false; // not registered
if (parseInt(this.user_id) !== 0) return false; // not primary account
if (user_id === 0) return false; // cannot delete primary account in this fashion
var userstore = this.getUserStoreDirectory(false, user_id);
if (this.fs.existsSync(userstore)) {
this.fs.rmSync(userstore, { recursive: true });
return true;
}
return false;
}
/**
* 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
// FileStore
store_dir += "FileStore" + this.path.sep;
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
// FileStore
store_dir += "FileStore" + this.path.sep;
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;
}
resetCookies() {
this.session_store.cookies = {};
// webtv likes to have at least one cookie in the list, set a dummy cookie for zefie's site expiring in 1 year.
this.addCookie("wtv.zefie.com", "/", this.wtvshared.getUTCTime(365 * 86400000), "cookie_type=chocolatechip");
}
addCookie(domain, path = null, expires = null, data = null) {
if (!this.checkCookies()) this.resetCookies();
if (!domain) return false;
else if (typeof (domain) == 'object') {
// accept array as first argument
if (domain.domain && domain.path && domain.expires && domain.data) var cookie_data = domain;
else return false;
} else {
if (path && expires && data) {
var cookie_data = new Array();
cookie_data['cookie'] = unescape(data);
cookie_data['expires'] = unescape(expires);
cookie_data['path'] = unescape(path);
cookie_data['domain'] = unescape(domain);
} else {
return false;
}
}
var self = this;
var cookie_index = -1;
// see if we have a cookie for this domain/path
Object.keys(this.session_store.cookies).forEach(function (k) {
if (cookie_index >= 0) return;
if (domain == self.session_store.cookies[k].domain && path == self.session_store.cookies[k].path) cookie_index = k;
});
// otherwise add a new one
if (cookie_index == -1) cookie_index = this.countCookies();
this.session_store.cookies[cookie_index] = Object.assign({}, cookie_data);
return true;
}
getCookie(domain, path) {
if (!this.checkCookies()) this.resetCookies();
var self = this;
var result = false;
Object.keys(this.session_store['cookies']).forEach(function (k) {
if (result != false) return;
if (self.session_store['cookies'][k].domain == domain &&
self.session_store['cookies'][k].path == path) {
var current_epoch_utc = Date.parse((new Date()).toUTCString());
var cookie_expires_epoch_utc = Date.parse(new Date(Date.parse(self.session_store['cookies'][k].expires)).toUTCString());
if (cookie_expires_epoch_utc <= current_epoch_utc) self.deleteCookie(self.session_store['cookies'][k]);
else result = self.session_store['cookies'][k];
}
});
return result;
}
getCookieString(domain, path) {
var cookie_data = this.getCookie(domain, path);
/*
var outstring = "";
Object.keys(cookie_data).forEach(function (k) {
outstring += k + "=" + escape(cookie_data[k]) + "&";
});
return outstring.substring(0, outstring.length - 1);
*/
return cookie_data.cookie;
}
deleteCookie(domain, path = null) {
var result = false;
if (!this.checkCookies()) {
this.resetCookies();
return true;
}
if (!domain) return false;
else if (typeof (domain) == 'object') {
// accept array as first argument
if (domain.domain && domain.path) {
path = domain.path;
domain = domain.domain;
}
} else if (!path) {
return false;
}
var self = this;
Object.keys(this.session_store['cookies']).forEach(function (k) {
if (self.session_store['cookies'][k].domain == domain && self.session_store['cookies'][k].path == path) {
delete self.session_store['cookies'][k];
self.storeSessionData();
result = true;
}
});
return result;
}
checkCookies() {
if (!this.session_store.cookies) return false;
else if (this.session_store.cookies == []) return false;
return true;
}
listCookies() {
if (!this.checkCookies()) this.resetCookies();
var outstring = "";
var self = this;
Object.keys(this.session_store.cookies).forEach(function (k) {
outstring += self.session_store.cookies[k].domain + "\0" + self.session_store.cookies[k].path + "\0";
});
return outstring;
}
loadSessionData(raw_data = false) {
try {
if (this.fs.lstatSync(this.getUserStoreDirectory() + "user" + this.user_id + ".json")) {
var json_data = this.fs.readFileSync(this.getUserStoreDirectory() + "user" + this.user_id + ".json", 'Utf8')
if (raw_data) return JSON.parse(json_data);
var session_data = JSON.parse(json_data);
this.session_store = session_data;
return true;
}
} 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.wtvshared.filterSSID(this.ssid), e);
// also wipe any existing session_store
this.session_store = {};
return false;
}
}
encryptPassword(passwd) {
return CryptoJS.AES.encrypt(passwd, this.cryptoKey).toString();
}
decryptPassword(crypt) {
return CryptoJS.AES.decrypt(crypt, this.cryptoKey).toString(CryptoJS.enc.Utf8);
}
encodePassword(passwd) {
var encoded_passwd = CryptoJS.SHA512(passwd);
return encoded_passwd.toString(CryptoJS.enc.Base64);
}
generatePassword(len) {
return CryptoJS.lib.WordArray.random(Math.round(len / 2)).toString(CryptoJS.enc.Hex);
}
setUserPassword(passwd) {
var encoded_passwd = this.encodePassword(passwd);
this.setSessionData("subscriber_password", encoded_passwd);
this.saveSessionData();
}
setUserSMTPPassword(passwd) {
var encoded_passwd = this.encryptPassword(passwd);
this.setSessionData("subscriber_smtp_password", encoded_passwd);
this.saveSessionData();
}
getUserSMTPPassword() {
return this.decryptPassword(this.setSessionData("subscriber_smtp_password"))
}
disableUserPassword() {
this.setSessionData("subscriber_password", null);
this.saveSessionData();
}
getUserPasswordEnabled() {
if (!this.minisrv_config.config.passwords.enabled) return false; // master config override
var enabled = this.getSessionData("subscriber_password");
return (enabled != null && typeof enabled != undefined); // true if set, false if null/disabled
}
validateUserPassword(passwd) {
if (!this.getUserPasswordEnabled()) return true; // no password is set so always validate
var encoded_passwd = this.encodePassword(passwd);
return (encoded_passwd == this.getSessionData("subscriber_password"));
}
isUserLoggedIn() {
if (!this.getUserPasswordEnabled()) return true; // no password is set so always validate
var password_valid = this.get("password_valid");
return (password_valid);
}
setUserLoggedIn(value) {
if (value) return this.set("password_valid", value);
else {
this.delete("password_valid");
return false;
}
}
saveSessionData(force_write = false, skip_merge = false) {
if (this.isRegistered()) {
if (!skip_merge) {
// load data from disk and merge new data
var temp_data = this.loadSessionData(true);
if (temp_data) this.session_store = Object.assign(temp_data, this.session_store);
temp_data = null;
}
} else {
// do not write file if user is not registered, return true because this is not an error
// 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 = (!skip_merge) ? this.loadSessionData(true) : {};
var storeDir = this.getUserStoreDirectory();
if (!this.fs.existsSync(storeDir)) this.mkdirRecursive(storeDir);
var sessionToStore = this.session_store;
if (sessionToStore.password_valid) delete sessionToStore.password_valid; // do not save validity state of password login, resets when session expires
if (json_save_data != json_load_data) this.fs.writeFileSync(storeDir + "user" + this.user_id + ".json", JSON.stringify(sessionToStore), "Utf8");
return true;
} catch (e) {
console.error(" # Error saving session data for", this.wtvshared.filterSSID(this.ssid), e);
return false;
}
}
retrieveSessionData() {
// alias
return this.loadSessionData();
}
storeSessionData(force_write = false) {
// alias
return this.saveSessionData(force_write);
}
SaveIfRegistered(skip_merge = false) {
if (this.isRegistered()) return this.saveSessionData(false, skip_merge);
return false;
}
isRegistered(session_mode = true) {
if (session_mode)
return (this.getSessionData("registered") && this.fs.existsSync(this.getUserStoreDirectory()));
else
return this.fs.existsSync(this.getUserStoreDirectory());
}
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.existsSync(user_store_base + ".json")) {
this.fs.unlinkSync(user_store_base + ".json");
this.session_store = {};
}
if (this.fs.existsSync(user_store_base)) {
this.fs.rmSync(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.wtvshared.filterSSID(this.ssid), e);
return false;
}
}
hasCap(cap) {
if (this.capabilities) {
return this.capabilities[cap] || false;
}
return false;
}
getMaxUsernameLength() {
if (parseInt(this.data_store['wtv-system-version'] < 4000)) {
// older builds may crash with nicknames longer than 16 chars.
// actual build where support started is yet unknown
return 16;
} else {
// newer builds supported up to 32 chars, I think
return 32;
}
}
setIRCNick(nick) {
// strip out unsupported chars
nick = nick.replace(/[^a-zA-Z0-9\-\_\`\^]/g, "");
// limit nick length based on build support
nick = nick.substring(0, this.getMaxUsernameLength());
// returns headers to send to client, while storing the new data in our session data.
this.data_store['wtv-user-name'] = nick;
this.data_store['wtv-irc-nick'] = nick;
this.session_store.subscriber_irc_nick = nick;
return "wtv-irc-nick: " + nick + "\nwtv-user-nick: " + nick;
}
isMiniBrowser() {
return (this.data_store['wtv-need-upgrade'] || this.data_store['wtv-used-8675309']) ? true : false;
}
currentConnections() {
if (this.data_store) {
if (this.data_store.sockets) {
return this.data_store.sockets.size;
}
}
return 0;
}
getSessionData(key = null) {
if (typeof (this.data_store) === 'session_store') return null;
else if (key === null) return this.data_store;
else if (this.session_store[key]) return this.session_store[key];
else return null;
}
setSessionData(key, value) {
if (key === null) throw ("ClientSessionData.set(): invalid key provided");
if (typeof (this.session_store) === 'undefined') this.session_store = new Array();
this.session_store[key] = value;
this.SaveIfRegistered();
}
deleteSessionData(key) {
if (key === null) throw ("ClientSessionData.delete(): invalid key provided");
delete this.session_store[key];
this.SaveIfRegistered(true);
}
get(key = null) {
if (typeof (this.data_store) === 'undefined') return null;
else if (key === null) return this.data_store;
else if (this.data_store[key]) return this.data_store[key];
else return null;
}
set(key, value) {
if (key === null) throw ("ClientSessionData.set(): invalid key provided");
if (typeof (this.data_store) === 'undefined') this.data_store = new Array();
this.data_store[key] = value;
this.SaveIfRegistered();
}
delete(key) {
if (key === null) throw ("ClientSessionData.delete(): invalid key provided");
delete this.data_store[key];
this.SaveIfRegistered(true);
}
getBoxName() {
switch (this.get("wtv-client-rom-type")) {
case "US-DTV-disk-0MB-16MB-softmodem-CPU5230":
case "US-DTV-disk-0MB-32MB-softmodem-CPU5230":
return "UltimateTV Satellite receiver";
case "US-WEBSTAR-disk-0MB-8MB-softmodem-CPU5230":
case "US-WEBSTAR-disk-0MB-16MB-softmodem-CPU5230":
return "WebTV Satellite receiver";
case "US-LC2-flashdisk-0MB-16MB-softmodem-CPU5230":
case "US-LC2-disk-0MB-8MB":
case "US-LC2-flash-2MB-8MB":
case "JP-LC2-disk-0MB-8MB":
case "JP-LC2-flash-2MB-8MB":
case "US-LC2-disk-0MB-8MB-softmodem-CPU5230":
case "US-LC2-flash-2MB-8MB-softmodem-CPU5230 ":
case "US-LC2-disk-0MB-8MB-CPU5230":
case "US-LC2-flash-2MB-8MB-CPU5230":
case "JP-LC2-disk-0MB-8MB-CPU5230":
case "JP-LC2-disk-0MB-16MB-CPU5230":
case "JP-LC2-flash-2MB-8MB-CPU5230":
return "WebTV Plus receiver";
default:
return "WebTV Internet receiver";
}
}
checkSecurity() {
var self = this;
var rejectReason = 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) {
if (subnet.indexOf('/') == -1) {
var mask, base_ip, long_ip = this.ip2long(ip);
var mask2, base_ip2, long_ip2 = this.ip2long(ip);
return (long_ip == long_ip2);
} else {
var mask, base_ip, long_ip = this.ip2long(ip);
if ((mask = subnet.match(/^(.*?)\/(\d{1,2})$/)) && ((base_ip = this.ip2long(mask[1])) >= 0)) {
var freedom = Math.pow(2, 32 - parseInt(mask[2]));
return (long_ip > base_ip) && (long_ip < base_ip + freedom - 1);
}
}
return false;
};
var rejectSSIDConnection = function (blacklist) {
if (blacklist) {
rejectReason = self.ssid + " is in the blacklist.";
console.log(" * Request from SSID", self.wtvshared.filterSSID(self.ssid), "(" + self.clientAddress + "), but that SSID is in the blacklist.");
} else {
rejectReason = self.ssid + " is not in the whitelist.";
console.log(" * Request from SSID", self.wtvshared.filterSSID(self.ssid), "(" + self.clientAddress + "), but that SSID is not in the whitelist.");
}
}
var checkSSIDIPWhitelist = function (ssid, blacklist) {
var ssid_access_list_ip_override = false;
if (self.minisrv_config.config.ssid_ip_allow_list) {
if (self.minisrv_config.config.ssid_ip_allow_list[self.ssid]) {
Object.keys(self.minisrv_config.config.ssid_ip_allow_list[self.ssid]).forEach(function (k) {
if (self.minisrv_config.config.ssid_ip_allow_list[self.ssid][k].indexOf('/') > 0) {
if (isInSubnet(self.clientAddress, self.minisrv_config.config.ssid_ip_allow_list[self.ssid][k])) {
// remoteAddr is in allowed subnet
ssid_access_list_ip_override = true;
}
} else {
if (self.clientAddress == self.minisrv_config.config.ssid_ip_allow_list[self.ssid][k]) {
// remoteAddr directly matches IP
ssid_access_list_ip_override = true;
}
}
});
if (!ssid_access_list_ip_override) rejectSSIDConnection(self.ssid, blacklist);
} else {
rejectSSIDConnection(blacklist);
}
} else {
rejectSSIDConnection(blacklist);
}
if (ssid_access_list_ip_override && self.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 (self.ssid && self.minisrv_config.config.ssid_allow_list) {
var ssid_is_in_whitelist = self.minisrv_config.config.ssid_allow_list.findIndex(element => element == self.ssid);
if (ssid_is_in_whitelist == -1) {
// no whitelist match, but lets see if the remoteAddress is allowed
checkSSIDIPWhitelist(self.ssid, false);
}
}
// now check blacklist
if (self.ssid && self.minisrv_config.config.ssid_block_list) {
var ssid_is_in_blacklist = self.minisrv_config.config.ssid_block_list.findIndex(element => element == self.ssid);
if (ssid_is_in_blacklist != -1) {
// blacklist match, but lets see if the remoteAddress is allowed
checkSSIDIPWhitelist(self.ssid, true);
}
}
if (rejectReason === null) {
// Passed Security
return true;
} else {
// Failed security
this.enableLockdown(rejectReason);
return false;
}
}
isAuthorized(url, whitelist = 'lockdown', ignore_lockdown = false) {
// not in lockdown so just return true
if (whitelist == 'lockdown' && !this.lockdown && !ignore_lockdown) return true;
// in lockdown, check whitelisted urls
var self = this;
var authorized = false;
switch (whitelist) {
case "lockdown":
Object.keys(this.lockdownWhitelist).forEach(function (k) {
if (self.lockdownWhitelist[k].charAt(self.lockdownWhitelist[k].length - 1) == '*') {
if (self.lockdownWhitelist[k].substring(0, self.lockdownWhitelist[k].length - 1) == url.substring(0, self.lockdownWhitelist[k].length - 1)) authorized = true;
} else {
if (self.lockdownWhitelist[k].substring(0, url.length) == url) authorized = true;
}
});
break;
case "login":
Object.keys(this.loginWhitelist).forEach(function (k) {
if (self.loginWhitelist[k].charAt(self.loginWhitelist[k].length - 1) == '*') {
if (self.loginWhitelist[k].substring(0, self.loginWhitelist[k].length - 1) == url.substring(0, self.loginWhitelist[k].length - 1)) authorized = true;
} else {
if (self.loginWhitelist[k].substring(0, url.length) == url) authorized = true;
}
});
break;
}
return authorized;
}
enableLockdown(reason) {
this.lockdown = true;
this.lockdownReason = reason;
}
disableLockdown() {
this.lockdown = false;
this.lockdownReason = null;
}
setClientAddress(addr) {
this.clientAddress = addr;
}
getClientAddress() {
return this.clientAddress;
}
setMailstore(mailstore) {
this.mailstore = mailstore;
}
getManufacturer(url = false) {
var isPlus = this.hasCap("client-has-tv-experience")
var romtype = this.get("wtv-client-rom-type");
var brandId = this.ssid.charAt(8)
if (brandId == 0)
if (url && romtype == "US-DTV-disk-0MB-32MB-softmodem-CPU5230")
return "Sony/DirecTV";
else
return "Sony";
else if (brandId == 1)
if (url && isPlus == true)
return "Philips-Plus";
else
return "Philips";
else if (brandId == 4)
return "Mitsubishi";
else if (brandId == 5)
return "Philips-Mont";
else if (brandId == 7)
return "Samsung";
else if (brandId == 9)
if (url)
if (romtype == "US-DTV-disk-0MB-32MB-softmodem-CPU5230")
return "Thomson/DirecTV";
else
return "Thomson";
else
return "RCA";
else
return "WebTV";
}
}
module.exports = WTVClientSessionData;

View File

@@ -0,0 +1,355 @@
/**
* 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|null} group Group to which it belongs
* @param {string|null} original_filename Use this filename (useful if WTV GZ)
*/
delete(path, group = null, original_filename = null) {
path = this.checkOriginalName(path, original_filename);
this.download_list += "DELETE " + path + "\n";
if (group) this.download_list += "group: " + group + "\n\n";
else (this.download_list) += "\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|null} checksum md5sum of the file
* @param {string|null} uncompressed_size Uncompressed size of gzip file
* @param {string|null} original_filename Use this filename (useful if WTV GZ)
* @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";
source = source.replace(/\\/g, "/");
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";
}
/**
* Helper function for WTV GZIP, if original_name is set, use this filename instead of the name in the path
* @param {string} path
* @param {string} original_name
* @returns {string} Path, with filename replaces by original_name, or just path if original_name = null
*/
checkOriginalName(path, original_name) {
if (original_name) {
var tmp = this.wtvshared.getFilePath(path);
if (tmp.length > 0) return tmp + "/" + original_name;
return original_name
} else return path;
}
getGroupDataFromClientPost(post_data) {
if (typeof post_data == 'string') post_data = post_data.split("\n\n");
var group_data = [];
var i = 0;
post_data.forEach(function (v) {
if (v.substr(0, 4) == "file") {
var block_split = v.split("\n");
var group_data_entry = {};
group_data_entry.path = block_split[0];
block_split.forEach(function (block_section) {
if (block_section.indexOf(": ") > 0) {
var block_section_split = block_section.split(": ");
group_data_entry[block_section_split[0]] = block_section_split[1];
}
});
group_data[group_data_entry.group] = group_data_entry;
}
});
return group_data;
}
/**
* 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
* @param {string} original_filename Use this filename (useful if WTV GZ)
*/
rename(srcfile, destfile, srcgroup, destgroup, original_filename = null) {
if (original_filename) {
srcfile = this.checkOriginalName(srcfile, original_filename);
destfile = this.checkOriginalName(srcfile, original_filename);
}
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, dont_delete_files = 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);
if (force_update) url += "&force=" + force_update;
if (dont_delete_files) url += "&dont_delete_files=" + dont_delete_files;
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': "Okay",
'buttonaction1': "client:goback",
'noback': true,
}).getURL();
// End set defaults
return `<html>
<head>
<meta
http-equiv=refresh
content="0;url=client:Fetch?group=${escape(group)}&source=${escape(url)}&message=${escape(message)}"
>
<display downloadsuccess="${success_url}" downloadfail="${fail_url}">
<title>${title}</title>
</head>
<body bgcolor=#0 text=#42CC55 fontsize=large hspace=0 vspace=0>
<table cellspacing=0 cellpadding=0>
<tr>
<td width=104 height=74 valign=middle align=center bgcolor=3B3A4D>
<img src="${this.minisrv_config.config.service_logo}" width=86 height=64>
<td width=20 valign=top align=left bgcolor=3B3A4D>
<spacer>
<td colspan=2 width=436 valign=middle align=left bgcolor=3B3A4D>
<font color=D6DFD0 size=+2><blackface><shadow>
<spacer type=block width=1 height=4>
<br>
${message}
</shadow>
</blackface>
</font>
<tr>
<td width=104 height=20>
<td width=20>
<td width=416>
<td width=20>
<tr>
<td colspan=2>
<td>
<font size=+1>
${main_message}
<p>This may take a while.
</font>
<tr>
<td colspan=2>
<td>
<br><br>
<font color=white>
<progressindicator name="downloadprogress"
message="Preparing..."
height=40 width=250>
</font>
</table>
</body>
</html>
`
}
}
module.exports = WTVDownloadList;

View File

@@ -0,0 +1,395 @@
class WTVFavorites {
fs = require('fs');
path = require('path');
uuid = require('uuid');
ssid = null;
minisrv_config = [];
wtvshared = null;
wtvmime = null;
wtvclient = null;
WTVClientSessionData = null;
favFileExt = ".zfav";
favstore_dir = null;
folderArr = [];
messageArr = [];
constructor(minisrv_config, wtvclient) {
if (!minisrv_config) throw ("minisrv_config required");
if (!wtvclient) throw ("WTVClientSessionData required");
var WTVShared = require("./WTVShared.js")['WTVShared'];
var WTVMime = require("./WTVMime.js");
this.WTVClientSessionData = require("./WTVClientSessionData.js");
this.minisrv_config = minisrv_config;
this.wtvshared = new WTVShared(minisrv_config);
this.wtvmime = new WTVMime(minisrv_config);
this.wtvclient = wtvclient;
this.ssid = wtvclient.ssid;
this.folderArr = this.folderArr;
this.messageArr = this.messageArr;
}
checkFavIntroSeen() {
return (this.wtvclient.getSessionData("subscriber_fav_intro_seen")) ? this.wtvclient.getSessionData("subscriber_fav_intro_seen") : false;
}
setFavIntroSeen(seen) {
this.wtvclient.setSessionData("subscriber_fav_intro_seen", (seen) ? true : false);
}
favstoreExists() {
if (!this.isguest) {
if (this.favstore_dir === null) {
// set favstore directory local var so we don't call the function every time
var userstore_dir = this.wtvclient.getUserStoreDirectory();
// FavStore
var store_dir = "FavStore" + this.path.sep;
this.favstore_dir = userstore_dir + store_dir;
}
return this.fs.existsSync(this.favstore_dir);
}
return null;
}
folderExists(foldername) {
var folder_dir = null;
if (this.favstoreExists()) {
if (!foldername) return null;
var folder_dir = foldername + this.path.sep;
var store_dir = this.favstore_dir + folder_dir;
}
return (store_dir !== null) ? this.fs.existsSync(store_dir) : false;
}
getFolderDir(foldername) {
var folder_dir = null;
if (this.favstoreExists()) {
if (!foldername) return null;
var folder_dir = foldername + this.path.sep;
var store_dir = this.favstore_dir + folder_dir;
}
return store_dir;
}
createTemplateFolder(folder) {
// create emply folder
this.createFolder(folder)
var folder_templates = this.minisrv_config.favorites.folder_templates;
// populate it if a template exists
var self = this;
if (folder_templates[folder]) {
Object.keys(folder_templates[folder]).forEach(function (k) {
self.createFavorite(folder_templates[folder][k].title, folder_templates[folder][k].url, folder, (folder_templates[folder][k].image_type == "image/wtv-bitmap") ? atob(folder_templates[folder][k].image) : folder_templates[folder][k].image, folder_templates[folder][k].image_type);
})
}
}
createDefaultFolders() {
var brandId = this.ssid.charAt(8);
this.createTemplateFolder("Recommended");
if (brandId == 7)
this.createTemplateFolder("Personal (Samsung)");
else
this.createTemplateFolder("Personal");
if (brandId == 0)
this.createTemplateFolder("Sony");
}
createFavstore() {
if (this.favstoreExists() === false) {
if (!this.fs.existsSync(this.favstore_dir)) this.fs.mkdirSync(this.favstore_dir, { recursive: true });
this.createDefaultFolders();
this.wtvclient.setSessionData("subscriber_fav_images", true)
return true;
}
return false;
}
createFolder(foldername) {
var folder_exists = this.folderExists(foldername);
if (folder_exists === false) {
var folderdir = foldername + this.path.sep;
var store_dir = this.favstore_dir + folderdir;
if (!this.fs.existsSync(store_dir)) this.fs.mkdirSync(store_dir, { recursive: true });
return true;
}
return folder_exists;
}
getFolders() {
var path = this.favstore_dir;
var self = this;
return this.fs.readdirSync(path).filter(function (file) {
self.folderArr.push(file);
return self.folderArr;
});
}
createFavoriteID() {
return this.uuid.v1();
}
createFavorite(title, url, folder, image, imagetype) {
var folderpath = this.getFolderDir(folder);
var favoriteid = this.createFavoriteID();
var favoritefile = favoriteid + this.favFileExt;
var favoritefileout = folderpath + favoritefile;
if (imagetype != "url")
image = btoa(image);
title = decodeURIComponent(title).replaceAll("+", " ");
url = decodeURIComponent(url)
var favoritedata = {
"title": title,
"url": url,
"folder": folder,
"image": image,
"imagetype": imagetype,
"id": favoriteid
}
try {
if (this.fs.existsSync(favoritefileout)) {
console.log(" * ERROR: Favorite with this UUID (" + favoriteid + ") already exists (should never happen). Favorite lost.");
return false;
}
// encode favorite into json
var result = this.fs.writeFileSync(favoritefileout, JSON.stringify(favoritedata));
if (!result) return false;
} catch (e) {
console.error(" # FavErr: Favorite Store failed\n", e, "\n", favoritefileout, "\n", favorite, "\n");
}
return false;
}
listFavorites(folder) {
var folderpath = this.getFolderDir(folder);
var self = this;
self.messageArr = [];
this.fs.readdirSync(folderpath)
.map(function (v) {
var favorite_data_raw = null;
var favoritepath = folderpath + self.path.sep + v;
if (self.fs.existsSync(favoritepath)) favorite_data_raw = self.fs.readFileSync(favoritepath);
if (favorite_data_raw) {
var favorite_data = JSON.parse(favorite_data_raw);
self.messageArr.push(favorite_data);
}
})
return self.messageArr;
}
getFavorite(folder, favoriteid) {
var folder_path = this.getFolderDir(folder);
var folder_file = favoriteid + this.favFileExt;
var folder_file_in = folder_path + this.path.sep + folder_file;
var folder_data_raw = null;
if (this.fs.existsSync(folder_file_in)) folder_data_raw = this.fs.readFileSync(folder_file_in);
else console.error(" # FavErr: could not find ", folder_file_in);
if (folder_data_raw) {
var folder_data = JSON.parse(folder_data_raw);
folder_data.folder_path = folder_path;
folder_data.folder_file = folder_file;
if (folder_data) {
folder_data.id = favoriteid;
return folder_data;
}
else console.error(" # FavErr: could not parse json in ", folder_file_in);
}
return false;
}
deleteFolder(folder){
var dir = this.getFolderDir(folder);
if (dir) {
try {
this.fs.rmdirSync(dir, { recursive: true });
return true;
} catch (e) {
return false;
}
}
return false;
}
checkFolderName(folder) {
var check1 = /^([A-Za-z0-9\-\_])$/.test(folder);
var check2 = /^[A-Za-z]/.test(folder);
return (check1 && check2);
}
deleteFavorite(favoriteid, folder) {
var folderdir = this.getFolderDir(folder);
this.fs.unlinkSync(folderdir + favoriteid + ".zfav", { recursive: true });
}
clearFolder(folder) {
const { readdirSync, rmSync } = require('fs');
var dir = this.getFolderDir(folder);
readdirSync(dir).forEach(f => rmSync(`${dir}${f}`));
}
updateFavorite(favoritedata, folder) {
// encode message into json
var favoriteout = new Object();
var folderpath = this.getFolderDir(folder);
Object.assign(favoriteout, favoritedata);
delete favoriteout.folderpath;
delete favoriteout.favoritefile;
var result = this.fs.writeFileSync(folderpath + favoritedata.id + ".zfav", JSON.stringify(favoriteout));
if (!result) return false;
}
changeFavoriteName(favoriteid, folder, name) {
var favorite = this.getFavorite(folder, favoriteid);
if (!favorite) return false;
favorite.title = name;
this.updateFavorite(favorite, folder);
return true;
}
moveFavorite(oldfolder, newfolder, favoriteid) {
var favorite = this.getFavorite(oldfolder, favoriteid);
if (!favorite) return false;
var newfolderdata = this.listFavorites(newfolder);
var newfoldernum = newfolderdata.length
if (newfoldernum > 17)
return;
favorite.folder = newfolder;
this.updateFavorite(favorite, oldfolder);
var favoriteout = new Object();
var folderpath = this.getFolderDir(newfolder);
Object.assign(favoriteout, favorite);
delete favoriteout.folderpath;
delete favoriteout.favoritefile;
this.fs.writeFileSync(folderpath + favorite.id + ".zfav", JSON.stringify(favoriteout));
this.deleteFavorite(favoriteid, oldfolder)
return true;
}
createShortcutKey() {
var favoritefileout = this.favstore_dir + "KeyStore.zfav";
var keydata = {};
keydata.F1 = {
folder: "none",
id: "none"
}
keydata.F2 = {
folder: "none",
id: "none"
}
keydata.F3 = {
folder: "none",
id: "none"
}
keydata.F4 = {
folder: "none",
id: "none"
}
keydata.F5 = {
folder: "none",
id: "none"
}
keydata.F6 = {
folder: "none",
id: "none"
}
keydata.F7 = {
folder: "none",
id: "none"
}
try {
// encode favorite into json
var result = this.fs.writeFileSync(favoritefileout, keydata);
if (!result) return false;
} catch (e) {
console.error(" # FavErr: Key Store failed\n", e, "\n", favoritefileout);
}
return false;
}
updateShortcutKey(oldkey, newkey, folder, id) {
var folderpath = this.getFolderDir(folder);
var favoritefileout = this.favstore_dir + "KeyStore.zfav";
var keydata = {};
keydata = this.fs.readFileSync(favoritefileout)
console.log(newkey)
switch(newkey) {
case "F1":
keydata.F1.folder = folder;
break
case "F2":
keydata.F2 = {
folder: folder,
id: id
}
break;
case "F3":
keydata.F3 = {
folder: folder,
id: id
}
break;
case "F4":
keydata.F4 = {
folder: folder,
id: id
}
break;
case "F5":
keydata.F5 = {
folder: folder,
id: id
}
break;
case "F6":
keydata.F6 = {
folder: folder,
id: id
}
break;
case "F7":
keydata.F7 = {
folder: folder,
id: id
}
break;
}
if (oldkey == "none")
{
//no
} else {
keydata[oldkey].folder = null;
keydata[oldkey].id = null;
}
try {
// encode favorite into json
var result = this.fs.writeFileSync(favoritefileout, keydata);
if (!result) return false;
} catch (e) {
console.error(" # FavErr: Key Store failed\n", e, "\n", favoritefileout);
}
return false;
}
}
module.exports = WTVFavorites;

View File

@@ -0,0 +1,196 @@
class WTVFlashrom {
fs = require('fs');
https = require('https');
use_zefie_server = true;
bf0app_update = false;
service_vaults = new Array();
no_debug = false;
service_name = "";
minisrv_config = [];
wtvshared = null;
constructor(minisrv_config, service_vaults, service_name, use_zefie_server = true, bf0app_update = false, no_debug = false) {
var { WTVShared } = require("./WTVShared.js");
this.service_vaults = service_vaults;
this.service_name = service_name;
this.use_zefie_server = use_zefie_server;
this.bf0app_update = bf0app_update;
this.no_debug = no_debug;
this.minisrv_config = minisrv_config;
this.wtvshared = new WTVShared(minisrv_config);
}
async doLocalFlashROM(flashrom_file_path, request_path, callback, info_only = false) {
// use local flashrom files;
var self = this;
try {
this.fs.readFile(flashrom_file_path, null, function (err, data) {
if (err) {
errpage = wtvshared.doErrorPage(400)
var headers = errpage[0];
data = err.toString();
callback(data, headers);
} else {
if (info_only) {
callback(self.getFlashromInfo(data, request_path));
} else {
self.sendToClient(data, request_path, callback);
}
}
});
} catch (e) {
var errpage = this.wtvshared.doErrorPage(404, "The service could not find the requested ROM.")
var headers = errpage[0];
var data = errpage[1];
callback(data, headers);
}
}
formatPartNum(partnum) {
if (partnum < 10) return "00" + partnum; // 1s
else if (partnum >= 10 && partnum < 100) return "0" + partnum; // 10s
else return partnum; // 100s
}
getFlashromInfo(data, path) {
var flashrom_info = new Array();
var flashrom_magic = "96031889";
var part_header = new Buffer.alloc(32);
data.copy(part_header, 0, 0, 32);
flashrom_info.header_length = data.readUInt16BE(26);
flashrom_info.is_bootrom = (/\.brom$/).test(path);
// re-read entire header
var part_header = new Buffer.alloc(flashrom_info.header_length);
data.copy(part_header, 0, 0, flashrom_info.header_length);
flashrom_info.magic = part_header.toString('hex', 0, 4);
flashrom_info.valid_flashrom = false;
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.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);
flashrom_info.compression_type = parseInt(part_header[16], 16);
//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.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;
flashrom_info.total_parts_size = data.readUInt32BE(32);
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.minisrv_config.config.debug_flags.quiet && !this.no_debug) console.log(" # Flashrom Part Size :", flashrom_info.part_total_size);
if (this.minisrv_config.config.debug_flags.debug && !this.minisrv_config.config.debug_flags.quiet && !this.no_debug) console.log(" # Flashrom Bytes Sent :", flashrom_info.byte_progress);
if (this.minisrv_config.config.debug_flags.debug && !this.minisrv_config.config.debug_flags.quiet && !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.minisrv_config.config.debug_flags.quiet && !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.minisrv_config.config.debug_flags.debug && !this.minisrv_config.config.debug_flags.quiet && !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.minisrv_config.config.debug_flags.quiet && !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.minisrv_config.config.debug_flags.quiet && !this.no_debug) console.log(" # Flashrom Next Part Number :", flashrom_info.next_part_number);
}
if (this.minisrv_config.config.debug_flags.debug && this.minisrv_config.config.debug_flags.quiet) console.log(" # Sending", (flashrom_info.is_last_part) ? "Last Flashrom" : "Flashrom", "Part", flashrom_info.part_number, "- Bytes Sent:", flashrom_info.byte_progress + flashrom_info.part_total_size, "of", flashrom_info.total_parts_size, "(" + flashrom_info.percent_complete + " % complete)");
// 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.rompath = `wtv-flashrom:/${path}`;
if (flashrom_info.is_last_part && this.bf0app_update) {
flashrom_info.next_rompath = null;
} else if (flashrom_info.is_last_part && !this.bf0app_update) {
flashrom_info.next_rompath = "wtv-flashrom:/lc2-download-complete?";
} else {
flashrom_info.next_part_number = this.formatPartNum(flashrom_info.part_number + 1);
flashrom_info.next_rompath = flashrom_info.rompath.replace("part" + this.formatPartNum(flashrom_info.part_number), "part" + flashrom_info.next_part_number);
}
return flashrom_info;
}
async sendToClient(data, request_path, callback) {
var headers = "200 OK\n";
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 && this.bf0app_update) headers += "\nwtv-visit: " + flashrom_info.next_rompath;
headers += "\nminisrv-no-mail-count: true";
callback(data, headers);
}
async getFlashromMeta(request_path, callback) {
// read 512 bytes of rom, and send result of getFlashromInfo
// to callback (in data), without headers
this.getFlashRom(request_path, callback, 512);
}
async getFlashRom(request_path, callback, length = 0) {
var headers, flashrom_file_path = null;
var self = this;
Object.keys(self.service_vaults).forEach(function (g) {
if (flashrom_file_path != null) return;
flashrom_file_path = self.service_vaults[g] + "/" + self.service_name + "/" + request_path;
if (!self.fs.existsSync(flashrom_file_path)) flashrom_file_path = null;
});
if (this.use_zefie_server && !flashrom_file_path) {
// get flashrom files from archive.midnightchannel.net
var options = {
host: "archive.midnightchannel.net",
path: "/zefie/files/wtv-flashrom/" + request_path,
timeout: 5000,
method: 'GET'
}
if (length > 0) {
options.headers = {
'Range': 'bytes=0-' + length
}
}
const req = this.https.request(options, function (res) {
var data_hex = '';
res.setEncoding('hex');
res.on('data', d => {
data_hex += d;
})
res.on('end', function () {
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) {
var data = self.getFlashromInfo(Buffer.from(data_hex, 'hex'), request_path);
} else if (res.statusCode == 404) {
console.log(request_path);
var errpage = self.wtvshared.doErrorPage(404, "The service could not find the requested ROM on zefie's server.")
headers = errpage[0];
var data = errpage[1];
} else {
var errpage = self.wtvshared.doErrorPage(400)
headers = errpage[0];
var data = errpage[1];
}
if (!headers && res.statusCode != 206) {
self.sendToClient(data, request_path, callback);
} else {
callback(data, headers);
}
});
});
req.end();
} else {
this.doLocalFlashROM(flashrom_file_path, request_path, callback, ((length != 0) ? true : false));
}
}
}
module.exports = WTVFlashrom;

View File

@@ -0,0 +1,196 @@
class WTVGuide {
minisrv_config = null;
session_data = null;
wtvshared = null;
runScriptInVM = null;
fs = require('fs');
constructor(minisrv_config, session_data, socket, runScriptInVM) {
if (!minisrv_config) throw ("minisrv_config required");
if (!session_data) throw ("WTVClientSessionData required");
var WTVShared = require("./WTVShared.js")['WTVShared'];
this.minisrv_config = minisrv_config;
this.session_data = session_data;
this.wtvshared = new WTVShared(minisrv_config);
this.runScriptInVM = runScriptInVM;
}
generatePage(topic, subtopic, page = null) {
// sanitize a bit
var template = null;
var template_args = null;
var data = false;
switch (topic.toLowerCase()) {
case "glossary":
var template = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/templates/glossary.js");
var glossary_datafile = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/glossary.json");
if (!this.fs.existsSync(template)) break;
if (!this.fs.existsSync(glossary_datafile)) break;
var glossary = JSON.parse(this.fs.readFileSync(glossary_datafile));
if (glossary[subtopic.toUpperCase()]) {
if (page) {
// glossary word
if (glossary[subtopic.toUpperCase()][page.toLowerCase()]) {
var word = glossary[subtopic.toUpperCase()][page.toLowerCase()].word;
var definition = glossary[subtopic.toUpperCase()][page.toLowerCase()].definition;
// replace <word>the word</word> with a nice convienent link
var search = "<word";
while (definition.indexOf(search) >= 0) {
var link_word_for_link, link_word_start_letter, link_word_override = null;
var original_start, end = 0;
var start = definition.indexOf(search) + search.length;
original_start = start;
// handle <word="whatever">
console.log("start:", start)
console.log("debug > position:", definition.substr(start, 1));
if (definition.substr(start, 1) != ">") {
console.log("debug: detecting word in tag")
start++; // +1 to skip =
end = definition.indexOf(">", start);
console.log("debug > position 2:", definition.indexOf(">", start));
link_word_override = definition.substring(start, end);
// strip any quotes
if (link_word_override.substr(0, 1).match(/[\"\']/)) link_word_override = link_word_override.substring(1);
if (link_word_override.substr(link_word_override.length - 1, 1).match(/[\"\']/)) link_word_override = link_word_override.substr(0, link_word_override.length - 1);
link_word_for_link = link_word_override.replace(/ /g, '').replace(/\'/g, '').replace(/\"/g, '').toLowerCase();
link_word_start_letter = link_word_for_link.substr(0, 1).toUpperCase();
start = end + 1; // update start pos for rest of processing
} else {
console.log("debug: generating word")
start++;
}
console.log("end:", end)
end = definition.indexOf("</word>", start);
var link_word = definition.substring(start, end);
if (!link_word_for_link) link_word_for_link = link_word.replace(/ /g, '').replace(/\'/g,'').replace(/\"/g,'').toLowerCase();
if (!link_word_start_letter) link_word_start_letter = link_word.substr(0, 1).toUpperCase();
if (!link_word_override) link_word_override = link_word;
var link = `wtv-guide:/help?topic=Glossary&subtopic=${link_word_start_letter}&page=${link_word_for_link}&word=${encodeURIComponent(link_word_override)}`
var new_definition = definition.substring(0, original_start - search.length) + `<a href="${link}">${link_word}</a>` + definition.substring(end + 7);
console.log("start:", start)
console.log("end:", end)
console.log("link_word:", link_word)
console.log("link_word_for_link:", link_word_for_link);
console.log("link_word_start_letter:", link_word_start_letter);
console.log("link:", link)
console.log("new_definition:", new_definition)
definition = new_definition;
}
// replaces <boxname> with the friendly name of the type of unit the user has
while (definition.indexOf("<boxname>") >= 0) {
var romtype = this.session_data.get("wtv-client-rom-type");
var boxname = "";
if (romtype == "US-WEBSTAR-disk-0MB-16MB-softmodem-CPU5230" || romtype == "US-DTV-disk-0MB-32MB-softmodem-CPU5230") boxname = "satellite receiver"
else if (this.session_data.hasCap("client-has-tv-experience")) boxname = "WebTV Plus receiver";
else boxname = "WebTV Internet terminal";
definition = definition.replace(/\<boxname\>/g, boxname);
}
// replaces <boxname_plus> with either "WebTV" or "WebTV Plus" depending on user box type
while (definition.indexOf("<boxname_plus>") >= 0) {
var boxname = "WebTV";
if (this.session_data.hasCap("client-has-tv-experience")) boxname += " Plus";
definition = definition.replace(/\<boxname\_plus\>/g, boxname);
}
// replaces <webhome> with either "Home" or "Web Home" depending on user box type
while (definition.indexOf("<webhome>") >= 0) {
var homename = "Home";
if (this.session_data.hasCap("client-has-tv-experience")) homename = "Web " + homename;
definition = definition.replace(/\<webhome\>/g, homename);
}
template_args = {
minisrv_config: this.minisrv_config,
word: word,
definition: definition
}
}
} else {
// glossary letter word index
var template = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/templates/glossary_word_index.js");
var isPlusBox = false;
if (this.session_data.hasCap("client-has-tv-experience")) isPlusBox = true;
var worddb = [];
Object.keys(glossary[subtopic.toUpperCase()]).forEach(function (k) {
if (glossary[subtopic.toUpperCase()][k].plusonly && !isPlusBox) return;
var thisword = glossary[subtopic.toUpperCase()][k];
thisword.link = k;
worddb.push(thisword);
})
template_args = {
minisrv_config: this.minisrv_config,
letter: subtopic.toUpperCase(),
words: worddb
}
}
}
if (template) break;
case "index":
switch (subtopic.toLowerCase()) {
case "glossary":
var template = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/templates/glossary_index.js");
console.log(template);
var glossary_datafile = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/glossary.json");
if (!this.fs.existsSync(template)) break;
if (!this.fs.existsSync(glossary_datafile)) break;
var glossary = JSON.parse(this.fs.readFileSync(glossary_datafile));
var letters = [];
Object.keys(glossary).forEach(function (k) { letters.push(k); });
template_args = {
minisrv_config: this.minisrv_config,
letters: letters
}
}
if (template) break;
default:
// fallback to old js file method
try {
var prerendered = null;
if (!page) prerendered = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/prerendered/" + topic + "/" + subtopic + ".js");
else prerendered = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/prerendered/" + topic + "/" + subtopic + "/" + page + ".js");
if (!this.fs.existsSync(prerendered)) break;
var prerendered_jscode = this.fs.readFileSync(prerendered);
if (!prerendered_jscode) break;
prerendered_jscode = prerendered_jscode.toString('ascii');
var contextObj = {
"session_data": this.session_data
}
var vmResult = this.runScriptInVM(prerendered_jscode, contextObj);
if (vmResult.data) return vmResult.data;
} catch (e) {
console.log(e);
}
break;
}
if (template && template_args) {
if (!data) {
var WTVTemplate = require(template); // load template class
try {
var wtvt = new WTVTemplate(template_args); // initialize template with our args
data = wtvt.getTemplatePage(); // execute template function
} catch (e) {
console.log(" * wtv-template error:", e)
}
// unload and clean up module
wtvshared.unloadModule(template);
}
// return generated page
return data;
} else return false;
}
}
module.exports = WTVGuide;

View File

@@ -0,0 +1,522 @@
/**
* Pure-JS implementation of WebTV's LZPF compression
*
* This compression algorithm is based on LZP by Charles Bloom and was originally written for server to client communication by Andy McFadden
* This uses a (static) Huffman dictionary that was tuned for character occurances in a typical HTML page at the time (around 1996-1997).
*
* Andy McFadden:
* https://fadden.com/
* LZP:
* https://cbloom.com/src/index_lz.html
* https://en.wikibooks.org/wiki/Data_Compression/Dictionary_compression#LZP
*
* I wouldn't recommend using LZPF on anything but HTML and other text-based data (unless the data has many repeating bytes)
* LZPF can be replaced with gzip for LC2 and newer boxes. Classic is stuck with LZPF.
*
* Reverse engineered and ported by: Eric MacDonald (eMac)
* Modified By: zefie
**/
class WTVLzpf {
// Note: currentlty doesn't offer optimal streaming support but this is good enough to meet perf demands at the scale we're at.
current_bit_length = 0;
current_bits = 0;
ring_bufer_index = 0xFFFF;
working_data = 0;
match_index = 0;
compression_mode = 0;
checksum = 0;
filler_byte = 0x20
hash_table = new Uint16Array(0x1000)
ring_buffer = new Uint8Array(0x2000)
encoded_data = [];
/**
* This is used to encode (one-byte) literals with no previous tracked occurence.
*
* - Bytes with best compression: SPACE and LF and e"/<>Tainoprst
* - Bytes with good compression: TAB and ,-.1=ABCDEFGHILNOPRSbcdfghlmuw
* - Bytes that don't change the length of the bit stream: 024:MW_kvy
* - The rest will increase the length of bit stream
*
* I don't know what process they used to build this table. I assume they
* frequency-scanned a bunch of HTML files they had.
*
* Using Windows-1252 (based off of ISO-8859-1) chracter encoding to fill in this table. Didn't
* seem like they used a different table for Japan builds (ISO-2022-JP).
**/
nomatchEncode = [
/* [FLATTENED HUFFMAN CODE, CODE BIT LENGTH] */
[0x0000, 0x10] /* NUL */, [0x0001, 0x10] /* SOH */, [0x0002, 0x10] /* STX */,
[0x0003, 0x10] /* ETX */, [0x0004, 0x10] /* EOT */, [0x009A, 0x0F] /* ENQ */,
[0x0005, 0x10] /* ACK */, [0x009C, 0x0F] /* BEL */, [0x009E, 0x0F] /* BS */,
[0x3400, 0x06] /* TAB */, [0x7000, 0x05] /* LF */, [0x00A0, 0x0F] /* VT */,
[0x0006, 0x10] /* FF */, [0x0380, 0x09] /* CR */, [0x0007, 0x10] /* SO */,
[0x0008, 0x10] /* SI */, [0x0009, 0x10] /* DLE */, [0x000A, 0x10] /* DC1 */,
[0x000B, 0x10] /* DC2 */, [0x000C, 0x10] /* DC3 */, [0x000D, 0x10] /* DC4 */,
[0x000E, 0x10] /* NAK */, [0x000F, 0x10] /* SYN */, [0x00A2, 0x0F] /* BTB */,
[0x0010, 0x10] /* CAN */, [0x0011, 0x10] /* EM */, [0x0012, 0x10] /* SUB */,
[0x0013, 0x10] /* ESC */, [0x0014, 0x10] /* FS */, [0x0015, 0x10] /* GS */,
[0x0016, 0x10] /* RS */, [0x0017, 0x10] /* US */, [0xE000, 0x04] /* SPACE */,
[0x0200, 0x0A] /* ! */, [0x7800, 0x05] /* " */, [0x0400, 0x09] /* # */,
[0x00B0, 0x0E] /* $ */, [0x0018, 0x10] /* % */, [0x0120, 0x0B] /* & */,
[0x0480, 0x09] /* ' */, [0x0140, 0x0B] /* ( */, [0x0160, 0x0B] /* ) */,
[0x0240, 0x0A] /* * */, [0x00B8, 0x0D] /* + */, [0x1400, 0x07] /* , */,
[0x1600, 0x07] /* - */, [0x3800, 0x06] /* . */, [0x8000, 0x05] /* / */,
[0x0A00, 0x08] /* 0 */, [0x1800, 0x07] /* 1 */, [0x0B00, 0x08] /* 2 */,
[0x0500, 0x09] /* 3 */, [0x0C00, 0x08] /* 4 */, [0x0580, 0x09] /* 5 */,
[0x0600, 0x09] /* 6 */, [0x0680, 0x09] /* 7 */, [0x0700, 0x09] /* 8 */,
[0x0780, 0x09] /* 9 */, [0x0D00, 0x08] /* : */, [0x0180, 0x0B] /* ; */,
[0x8800, 0x05] /* < */, [0x3C00, 0x06] /* = */, [0x9000, 0x05] /* > */,
[0x0280, 0x0A] /* ? */, [0x00B4, 0x0E] /* @ */, [0x4000, 0x06] /* A */,
[0x1A00, 0x07] /* B */, [0x1C00, 0x07] /* C */, [0x1E00, 0x07] /* D */,
[0x4400, 0x06] /* E */, [0x2000, 0x07] /* F */, [0x2200, 0x07] /* G */,
[0x2400, 0x07] /* H */, [0x4800, 0x06] /* I */, [0x01A0, 0x0B] /* J */,
[0x02C0, 0x0A] /* K */, [0x2600, 0x07] /* L */, [0x0E00, 0x08] /* M */,
[0x4C00, 0x06] /* N */, [0x5000, 0x06] /* O */, [0x2800, 0x07] /* P */,
[0x00C0, 0x0C] /* Q */, [0x5400, 0x06] /* R */, [0x2A00, 0x07] /* S */,
[0x9800, 0x05] /* T */, [0x0800, 0x09] /* U */, [0x0880, 0x09] /* V */,
[0x0F00, 0x08] /* W */, [0x00D0, 0x0C] /* X */, [0x0300, 0x0A] /* Y */,
[0x0900, 0x09] /* Z */, [0x0019, 0x10] /* [ */, [0x001A, 0x10] /* \ */,
[0x001B, 0x10] /* ] */, [0x001C, 0x10] /* ^ */, [0x1000, 0x08] /* _ */,
[0x001D, 0x10] /* ` */, [0xA000, 0x05] /* a */, [0x2C00, 0x07] /* b */,
[0x5800, 0x06] /* c */, [0x5C00, 0x06] /* d */, [0xF000, 0x04] /* e */,
[0x2E00, 0x07] /* f */, [0x3000, 0x07] /* g */, [0x6000, 0x06] /* h */,
[0xA800, 0x05] /* i */, [0x01C0, 0x0B] /* j */, [0x1100, 0x08] /* k */,
[0x6400, 0x06] /* l */, [0x6800, 0x06] /* m */, [0xB000, 0x05] /* n */,
[0xB800, 0x05] /* o */, [0xC000, 0x05] /* p */, [0x01E0, 0x0B] /* q */,
[0xC800, 0x05] /* r */, [0xD000, 0x05] /* s */, [0xD800, 0x05] /* t */,
[0x3200, 0x07] /* u */, [0x1200, 0x08] /* v */, [0x6C00, 0x06] /* w */,
[0x0980, 0x09] /* x */, [0x1300, 0x08] /* y */, [0x0340, 0x0A] /* z */,
[0x00E0, 0x0C] /* { */, [0x00F0, 0x0C] /* | */, [0x0100, 0x0C] /* } */,
[0x0110, 0x0C] /* ~ */, [0x001E, 0x10] /* DEL */, [0x001F, 0x10] /* <20> */,
[0x0020, 0x10] /* */, [0x0021, 0x10] /* <20> */, [0x0022, 0x10] /* <20> */,
[0x0023, 0x10] /* <20> */, [0x0024, 0x10] /* <20> */, [0x0025, 0x10] /* <20> */,
[0x0026, 0x10] /* <20> */, [0x0027, 0x10] /* <20> */, [0x0028, 0x10] /* <20> */,
[0x0029, 0x10] /* <20> */, [0x002A, 0x10] /* <20> */, [0x002B, 0x10] /* <20> */,
[0x002C, 0x10] /* */, [0x002D, 0x10] /* <20> */, [0x002E, 0x10] /* */,
[0x002F, 0x10] /* */, [0x00A4, 0x0F] /* <20> */, [0x00A6, 0x0F] /* <20> */,
[0x00A8, 0x0F] /* <20> */, [0x0030, 0x10] /* <20> */, [0x0031, 0x10] /* <20> */,
[0x0032, 0x10] /* <20> */, [0x0033, 0x10] /* <20> */, [0x0034, 0x10] /* <20> */,
[0x0035, 0x10] /* <20> */, [0x0036, 0x10] /* <20> */, [0x0037, 0x10] /* <20> */,
[0x0038, 0x10] /* <20> */, [0x0039, 0x10] /* */, [0x003A, 0x10] /* <20> */,
[0x003B, 0x10] /* <20> */, [0x003C, 0x10] /* NBSP*/, [0x003D, 0x10] /* <20> */,
[0x003E, 0x10] /* <20> */, [0x003F, 0x10] /* <20> */, [0x0040, 0x10] /* <20> */,
[0x0041, 0x10] /* <20> */, [0x0042, 0x10] /* <20> */, [0x0043, 0x10] /* <20> */,
[0x0044, 0x10] /* <20> */, [0x0045, 0x10] /* <20> */, [0x0046, 0x10] /* <20> */,
[0x0047, 0x10] /* <20> */, [0x0048, 0x10] /* <20> */, [0x0049, 0x10] /* SHY */,
[0x004A, 0x10] /* <20> */, [0x004B, 0x10] /* <20> */, [0x004C, 0x10] /* <20> */,
[0x004D, 0x10] /* <20> */, [0x004E, 0x10] /* <20> */, [0x004F, 0x10] /* <20> */,
[0x0050, 0x10] /* <20> */, [0x0051, 0x10] /* <20> */, [0x0052, 0x10] /* <20> */,
[0x0053, 0x10] /* <20> */, [0x0054, 0x10] /* <20> */, [0x0055, 0x10] /* <20> */,
[0x0056, 0x10] /* <20> */, [0x0057, 0x10] /* <20> */, [0x0058, 0x10] /* <20> */,
[0x0059, 0x10] /* <20> */, [0x005A, 0x10] /* <20> */, [0x005B, 0x10] /* <20> */,
[0x005C, 0x10] /* <20> */, [0x005D, 0x10] /* <20> */, [0x005E, 0x10] /* <20> */,
[0x005F, 0x10] /* <20> */, [0x0060, 0x10] /* <20> */, [0x0061, 0x10] /* <20> */,
[0x0062, 0x10] /* <20> */, [0x00AA, 0x0F] /* <20> */, [0x0063, 0x10] /* <20> */,
[0x0064, 0x10] /* <20> */, [0x0065, 0x10] /* <20> */, [0x0066, 0x10] /* <20> */,
[0x0067, 0x10] /* <20> */, [0x0068, 0x10] /* <20> */, [0x0069, 0x10] /* <20> */,
[0x006A, 0x10] /* <20> */, [0x006B, 0x10] /* <20> */, [0x006C, 0x10] /* <20> */,
[0x006D, 0x10] /* <20> */, [0x006E, 0x10] /* <20> */, [0x006F, 0x10] /* <20> */,
[0x0070, 0x10] /* <20> */, [0x0071, 0x10] /* <20> */, [0x0072, 0x10] /* <20> */,
[0x0073, 0x10] /* <20> */, [0x0074, 0x10] /* <20> */, [0x0075, 0x10] /* <20> */,
[0x0076, 0x10] /* <20> */, [0x0077, 0x10] /* <20> */, [0x0078, 0x10] /* <20> */,
[0x0079, 0x10] /* <20> */, [0x007A, 0x10] /* <20> */, [0x007B, 0x10] /* <20> */,
[0x007C, 0x10] /* <20> */, [0x007D, 0x10] /* <20> */, [0x007E, 0x10] /* <20> */,
[0x007F, 0x10] /* <20> */, [0x0080, 0x10] /* <20> */, [0x0081, 0x10] /* <20> */,
[0x0082, 0x10] /* <20> */, [0x0083, 0x10] /* <20> */, [0x0084, 0x10] /* <20> */,
[0x0085, 0x10] /* <20> */, [0x0086, 0x10] /* <20> */, [0x0087, 0x10] /* <20> */,
[0x0088, 0x10] /* <20> */, [0x0089, 0x10] /* <20> */, [0x008A, 0x10] /* <20> */,
[0x008B, 0x10] /* <20> */, [0x008C, 0x10] /* <20> */, [0x008D, 0x10] /* <20> */,
[0x00AC, 0x0F] /* <20> */, [0x008E, 0x10] /* <20> */, [0x008F, 0x10] /* <20> */,
[0x0090, 0x10] /* <20> */, [0x0091, 0x10] /* <20> */, [0x0092, 0x10] /* <20> */,
[0x0093, 0x10] /* <20> */, [0x00AE, 0x0F] /* <20> */, [0x0094, 0x10] /* <20> */,
[0x0095, 0x10] /* <20> */, [0x0096, 0x10] /* <20> */, [0x0097, 0x10] /* <20> */,
[0x0098, 0x10] /* <20> */, [0x0099, 0x10]
];
/**
* This is the table that reduces the size based on repeated patterns in the file.
*
* When we find a byte match in the ring buffer we use this table to encode the length of the matched bytes.
*
* - These are intentionally 32-bit. The leftmost flag bit is 1 in each of these to tell the decoder to use match decoding.
* - LZP hash bits are used to encode the position where the matched bytes start.
* - We're allowed to match up to 298 bytes before we can't encode more (we need an entry in this table for each byte more).
* - We can reach for matches 65KB behind the current LZ cursor (65KB is the ring buffer size and highest a 16-bit hash can reach).
**/
matchEncode = [
/* [MATCH CODE, MATCH CODE BIT LENGTH] */
[0x80000000, 0x01], [0x80000000, 0x03],
[0xA0000000, 0x03], [0xC0000000, 0x03],
[0xE0000000, 0x06], [0xE4000000, 0x06],
[0xE8000000, 0x06], [0xEC000000, 0x06],
[0xF0000000, 0x06], [0xF4000000, 0x06],
[0xF8000000, 0x06], [0xFC000000, 0x0B],
[0xFC200000, 0x0B], [0xFC400000, 0x0B],
[0xFC600000, 0x0B], [0xFC800000, 0x0B],
[0xFCA00000, 0x0B], [0xFCC00000, 0x0B],
[0xFCE00000, 0x0B], [0xFD000000, 0x0B],
[0xFD200000, 0x0B], [0xFD400000, 0x0B],
[0xFD600000, 0x0B], [0xFD800000, 0x0B],
[0xFDA00000, 0x0B], [0xFDC00000, 0x0B],
[0xFDE00000, 0x0B], [0xFE000000, 0x0B],
[0xFE200000, 0x0B], [0xFE400000, 0x0B],
[0xFE600000, 0x0B], [0xFE800000, 0x0B],
[0xFEA00000, 0x0B], [0xFEC00000, 0x0B],
[0xFEE00000, 0x0B], [0xFF000000, 0x0B],
[0xFF200000, 0x0B], [0xFF400000, 0x0B],
[0xFF600000, 0x0B], [0xFF800000, 0x0B],
[0xFFA00000, 0x0B], [0xFFC00000, 0x0B],
[0xFFE00000, 0x13], [0xFFE02000, 0x13],
[0xFFE04000, 0x13], [0xFFE06000, 0x13],
[0xFFE08000, 0x13], [0xFFE0A000, 0x13],
[0xFFE0C000, 0x13], [0xFFE0E000, 0x13],
[0xFFE10000, 0x13], [0xFFE12000, 0x13],
[0xFFE14000, 0x13], [0xFFE16000, 0x13],
[0xFFE18000, 0x13], [0xFFE1A000, 0x13],
[0xFFE1C000, 0x13], [0xFFE1E000, 0x13],
[0xFFE20000, 0x13], [0xFFE22000, 0x13],
[0xFFE24000, 0x13], [0xFFE26000, 0x13],
[0xFFE28000, 0x13], [0xFFE2A000, 0x13],
[0xFFE2C000, 0x13], [0xFFE2E000, 0x13],
[0xFFE30000, 0x13], [0xFFE32000, 0x13],
[0xFFE34000, 0x13], [0xFFE36000, 0x13],
[0xFFE38000, 0x13], [0xFFE3A000, 0x13],
[0xFFE3C000, 0x13], [0xFFE3E000, 0x13],
[0xFFE40000, 0x13], [0xFFE42000, 0x13],
[0xFFE44000, 0x13], [0xFFE46000, 0x13],
[0xFFE48000, 0x13], [0xFFE4A000, 0x13],
[0xFFE4C000, 0x13], [0xFFE4E000, 0x13],
[0xFFE50000, 0x13], [0xFFE52000, 0x13],
[0xFFE54000, 0x13], [0xFFE56000, 0x13],
[0xFFE58000, 0x13], [0xFFE5A000, 0x13],
[0xFFE5C000, 0x13], [0xFFE5E000, 0x13],
[0xFFE60000, 0x13], [0xFFE62000, 0x13],
[0xFFE64000, 0x13], [0xFFE66000, 0x13],
[0xFFE68000, 0x13], [0xFFE6A000, 0x13],
[0xFFE6C000, 0x13], [0xFFE6E000, 0x13],
[0xFFE70000, 0x13], [0xFFE72000, 0x13],
[0xFFE74000, 0x13], [0xFFE76000, 0x13],
[0xFFE78000, 0x13], [0xFFE7A000, 0x13],
[0xFFE7C000, 0x13], [0xFFE7E000, 0x13],
[0xFFE80000, 0x13], [0xFFE82000, 0x13],
[0xFFE84000, 0x13], [0xFFE86000, 0x13],
[0xFFE88000, 0x13], [0xFFE8A000, 0x13],
[0xFFE8C000, 0x13], [0xFFE8E000, 0x13],
[0xFFE90000, 0x13], [0xFFE92000, 0x13],
[0xFFE94000, 0x13], [0xFFE96000, 0x13],
[0xFFE98000, 0x13], [0xFFE9A000, 0x13],
[0xFFE9C000, 0x13], [0xFFE9E000, 0x13],
[0xFFEA0000, 0x13], [0xFFEA2000, 0x13],
[0xFFEA4000, 0x13], [0xFFEA6000, 0x13],
[0xFFEA8000, 0x13], [0xFFEAA000, 0x13],
[0xFFEAC000, 0x13], [0xFFEAE000, 0x13],
[0xFFEB0000, 0x13], [0xFFEB2000, 0x13],
[0xFFEB4000, 0x13], [0xFFEB6000, 0x13],
[0xFFEB8000, 0x13], [0xFFEBA000, 0x13],
[0xFFEBC000, 0x13], [0xFFEBE000, 0x13],
[0xFFEC0000, 0x13], [0xFFEC2000, 0x13],
[0xFFEC4000, 0x13], [0xFFEC6000, 0x13],
[0xFFEC8000, 0x13], [0xFFECA000, 0x13],
[0xFFECC000, 0x13], [0xFFECE000, 0x13],
[0xFFED0000, 0x13], [0xFFED2000, 0x13],
[0xFFED4000, 0x13], [0xFFED6000, 0x13],
[0xFFED8000, 0x13], [0xFFEDA000, 0x13],
[0xFFEDC000, 0x13], [0xFFEDE000, 0x13],
[0xFFEE0000, 0x13], [0xFFEE2000, 0x13],
[0xFFEE4000, 0x13], [0xFFEE6000, 0x13],
[0xFFEE8000, 0x13], [0xFFEEA000, 0x13],
[0xFFEEC000, 0x13], [0xFFEEE000, 0x13],
[0xFFEF0000, 0x13], [0xFFEF2000, 0x13],
[0xFFEF4000, 0x13], [0xFFEF6000, 0x13],
[0xFFEF8000, 0x13], [0xFFEFA000, 0x13],
[0xFFEFC000, 0x13], [0xFFEFE000, 0x13],
[0xFFF00000, 0x13], [0xFFF02000, 0x13],
[0xFFF04000, 0x13], [0xFFF06000, 0x13],
[0xFFF08000, 0x13], [0xFFF0A000, 0x13],
[0xFFF0C000, 0x13], [0xFFF0E000, 0x13],
[0xFFF10000, 0x13], [0xFFF12000, 0x13],
[0xFFF14000, 0x13], [0xFFF16000, 0x13],
[0xFFF18000, 0x13], [0xFFF1A000, 0x13],
[0xFFF1C000, 0x13], [0xFFF1E000, 0x13],
[0xFFF20000, 0x13], [0xFFF22000, 0x13],
[0xFFF24000, 0x13], [0xFFF26000, 0x13],
[0xFFF28000, 0x13], [0xFFF2A000, 0x13],
[0xFFF2C000, 0x13], [0xFFF2E000, 0x13],
[0xFFF30000, 0x13], [0xFFF32000, 0x13],
[0xFFF34000, 0x13], [0xFFF36000, 0x13],
[0xFFF38000, 0x13], [0xFFF3A000, 0x13],
[0xFFF3C000, 0x13], [0xFFF3E000, 0x13],
[0xFFF40000, 0x13], [0xFFF42000, 0x13],
[0xFFF44000, 0x13], [0xFFF46000, 0x13],
[0xFFF48000, 0x13], [0xFFF4A000, 0x13],
[0xFFF4C000, 0x13], [0xFFF4E000, 0x13],
[0xFFF50000, 0x13], [0xFFF52000, 0x13],
[0xFFF54000, 0x13], [0xFFF56000, 0x13],
[0xFFF58000, 0x13], [0xFFF5A000, 0x13],
[0xFFF5C000, 0x13], [0xFFF5E000, 0x13],
[0xFFF60000, 0x13], [0xFFF62000, 0x13],
[0xFFF64000, 0x13], [0xFFF66000, 0x13],
[0xFFF68000, 0x13], [0xFFF6A000, 0x13],
[0xFFF6C000, 0x13], [0xFFF6E000, 0x13],
[0xFFF70000, 0x13], [0xFFF72000, 0x13],
[0xFFF74000, 0x13], [0xFFF76000, 0x13],
[0xFFF78000, 0x13], [0xFFF7A000, 0x13],
[0xFFF7C000, 0x13], [0xFFF7E000, 0x13],
[0xFFF80000, 0x13], [0xFFF82000, 0x13],
[0xFFF84000, 0x13], [0xFFF86000, 0x13],
[0xFFF88000, 0x13], [0xFFF8A000, 0x13],
[0xFFF8C000, 0x13], [0xFFF8E000, 0x13],
[0xFFF90000, 0x13], [0xFFF92000, 0x13],
[0xFFF94000, 0x13], [0xFFF96000, 0x13],
[0xFFF98000, 0x13], [0xFFF9A000, 0x13],
[0xFFF9C000, 0x13], [0xFFF9E000, 0x13],
[0xFFFA0000, 0x13], [0xFFFA2000, 0x13],
[0xFFFA4000, 0x13], [0xFFFA6000, 0x13],
[0xFFFA8000, 0x13], [0xFFFAA000, 0x13],
[0xFFFAC000, 0x13], [0xFFFAE000, 0x13],
[0xFFFB0000, 0x13], [0xFFFB2000, 0x13],
[0xFFFB4000, 0x13], [0xFFFB6000, 0x13],
[0xFFFB8000, 0x13], [0xFFFBA000, 0x13],
[0xFFFBC000, 0x13], [0xFFFBE000, 0x13],
[0xFFFC0000, 0x13], [0xFFFC2000, 0x13],
[0xFFFC4000, 0x13], [0xFFFC6000, 0x13],
[0xFFFC8000, 0x13], [0xFFFCA000, 0x13],
[0xFFFCC000, 0x13], [0xFFFCE000, 0x13],
[0xFFFD0000, 0x13], [0xFFFD2000, 0x13],
[0xFFFD4000, 0x13], [0xFFFD6000, 0x13],
[0xFFFD8000, 0x13], [0xFFFDA000, 0x13],
[0xFFFDC000, 0x13], [0xFFFDE000, 0x13],
[0xFFFE0000, 0x13], [0xFFFE2000, 0x13],
[0xFFFE4000, 0x13], [0xFFFE6000, 0x13],
[0xFFFE8000, 0x13], [0xFFFEA000, 0x13],
[0xFFFEC000, 0x13], [0xFFFEE000, 0x13],
[0xFFFF0000, 0x13], [0xFFFF2000, 0x13],
[0xFFFF4000, 0x13], [0xFFFF6000, 0x13],
[0xFFFF8000, 0x13], [0xFFFFA000, 0x13],
[0xFFFFC000, 0x13], [0xFFFFE000, 0x13],
// We never should select these. These were in the original executable so including them here.
[0x00000000, 0x00], [0x00000000, 0x00]
];
/**
* Initialize the Lzpf class.
*
* @returns {undefined}
*/
constructor() {
this.reset();
}
/**
* Sets starting values for the compression algorithm.
*
* @returns {undefined}
*/
reset() {
this.current_bit_length = 0;
this.current_bits = 0;
this.ring_bufer_index = 0xFFFF;
this.working_data = 0;
this.match_index = 0;
this.compression_mode = 0;
this.checksum = 0;
this.ring_buffer.fill(this.filler_byte, 0, 0x2000)
this.hash_table.fill(0xFFFF, 0, 0x1000);
this.encoded_data = [];
}
/**
* Appends a byte to the end of the compressed byte array. Re-allocates as needed
*
* @param byte {Number} char code of the byte to be added.
*
* @returns {undefined}
*/
AddByte(byte) {
this.encoded_data.push(byte);
}
/**
* Add bits onto the compressed bit stream.
*
* When we reach 8 bits we push a byte onto the compressed byte array.
*
* @param bits {Number} bits to add
* @param bit_length {Number} bit length
*
* @returns {undefined}
*/
AddBits(bits, bit_length) {
this.current_bits |= bits >>> (this.current_bit_length & 0x1F);
this.current_bit_length += bit_length;
while (this.current_bit_length > 7) {
this.AddByte((this.current_bits >>> 0x18) & 0xFF);
this.current_bit_length -= 8;
this.current_bits = (this.current_bits << 8) & 0xFFFFFFFF;
}
}
/**
* Starts a compression stream
*
* @returns {undefined} Lzpf compression data
*/
Begin() {
this.reset();
}
/**
* Encode a block of data. Used for streamed chunks.
*
* @param unencoded_data {Buffer} data to encode
* @param compress_data {Boolean} compress data
*
* @returns {Buffer} Lzpf encoded data
*/
EncodeBlock(unencoded_data, compress_data) {
this.encoded_data = [];
var uncompressed_len = unencoded_data.byteLength;
var i = 0;
var hash_index = 0;
while (i < uncompressed_len) {
var code_length = -1;
var code = -1;
var byte = unencoded_data.readUInt8(i);
this.ring_buffer[i & 0x1FFF] = byte;
if (this.match_index > 0) {
// Cozy time
if (byte != this.ring_buffer[this.ring_bufer_index] || this.match_index > 0x0127) {
// End of matching. Either we no longer match or we reached out limit.
code_length = this.matchEncode[this.match_index][1];
code = this.matchEncode[this.match_index][0];
this.match_index = 0;
this.compression_mode = 3;
} else {
// Previous iteration found a match so we continue matching until we can't.
this.match_index = (this.match_index + 1) & 0x1FFF;
this.ring_bufer_index = (this.ring_bufer_index + 1) & 0x1FFF;
this.checksum = (this.checksum + byte) & 0xFFFF;
this.working_data = ((this.working_data * 0x0100) + byte) & 0xFFFFFFFF;
i++;
}
} else {
this.ring_bufer_index = 0xFFFF;
if (i >= 3) {
// Start recoding data so we can lookup matches.
hash_index = (this.working_data >>> 0x0B ^ this.working_data) & 0x0FFF;
this.ring_bufer_index = this.hash_table[hash_index];
this.hash_table[hash_index] = i & 0x1FFF;
} else {
// The first three uncompressed bytes aren't used for the matching algorithm.
this.compression_mode++;
}
if (this.ring_bufer_index == 0xFFFF) {
// We never seen this byte before so we encode it with our Huffman table.
code_length = this.nomatchEncode[byte][1];
code = this.nomatchEncode[byte][0] << 0x10;
} else if (byte == this.ring_buffer[this.ring_bufer_index] && compress_data) {
// Wow dude, a match has been found. Let's switch get our own room in the next iteration to see if we match further.
this.match_index = 1;
this.ring_bufer_index = (this.ring_bufer_index + 1) & 0x1FFF;
this.compression_mode = 4;
} else {
// We've seen these bytes before but the index in the ring buffer doesn't match so we revert to our neat Huffman table
// We add 1 flag bit of 0 to account for the fact we've had a hash table hit but no hit in the ring buffer.
code_length = this.nomatchEncode[byte][1] + 1;
code = this.nomatchEncode[byte][0] << 0x0F;
}
this.checksum = (this.checksum + byte) & 0xFFFF;
// We work on a 2-byte context so we store the last two bytes so we can do cool lookups with it
this.working_data = ((this.working_data * 0x0100) + byte) & 0xFFFFFFFF;
i++;
}
if (code_length > 0) {
this.AddBits(code, code_length);
}
}
}
/**
* Ends a compression stream.
*
* @param compression_mode {Number} the end type used to finalize
*
* @returns {Buffer} Lzpf compression data
*/
Finish() {
var code_length = -1;
var code = -1;
if (this.compression_mode == 2) {
this.AddBits(0x00990000, 0x10);
} else if (this.compression_mode >= 3) {
if (this.compression_mode == 4) {
code_length = this.matchEncode[this.match_index][1];
code = this.matchEncode[this.match_index][0];
this.AddBits(code, code_length);
}
var hash_index = (this.working_data >>> 0x0B ^ this.working_data) & 0x0FFF;
var ring_bufer_index = this.hash_table[hash_index];
if (ring_bufer_index == 0xFFFF) {
this.AddBits(0x00990000, 0x10);
} else {
this.AddBits(0x004C8000, 0x11);
}
}
// Add checksum bits
this.AddBits((this.checksum << 0x10) & 0xFFFFFFFF, 0x08);
this.AddBits((this.checksum << 0x18) & 0xFFFFFFFF, 0x08);
// If we have leftover bits then add it.
if (this.current_bit_length > 0) {
this.AddByte((this.current_bits >>> 0x18) & 0xFF);
}
this.AddByte(this.filler_byte);
return Buffer.from(this.encoded_data);
}
/**
* Converts the data to a Javascript Buffer object
*
* @param data {String|Buffer} Data to convert
*
* @returns {Buffer} Javascript Buffer object
*/
ConvertToBuffer(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} data to compress
*
* @returns {Buffer} Lzpf compression data
*/
Compress(uncompressed_data) {
uncompressed_data = this.ConvertToBuffer(uncompressed_data);
this.Begin();
this.EncodeBlock(uncompressed_data, true);
return this.Finish();
}
}
module.exports = WTVLzpf;

View File

@@ -0,0 +1,510 @@
class WTVMail {
fs = require('fs');
path = require('path');
uuid = require('uuid');
ssid = null;
unread_mail = 0;
inbox_store = null;
sent_store = null;
saved_store = null;
minisrv_config = [];
wtvshared = null;
wtvmime = null;
wtvclient = null;
WTVClientSessionData = null;
mailstore_dir = null;
mailboxes = null;
msgFileExt = ".zmsg";
trashMailboxName = "Trash";
defaultColors = {};
sendmailDefaultBGColor = "#1F2033"
constructor(minisrv_config, wtvclient) {
if (!minisrv_config) throw ("minisrv_config required");
var WTVShared = require("./WTVShared.js")['WTVShared'];
var WTVMime = require("./WTVMime.js");
this.WTVClientSessionData = require("./WTVClientSessionData.js");
this.minisrv_config = minisrv_config;
this.wtvshared = new WTVShared(minisrv_config);
this.wtvmime = new WTVMime(minisrv_config);
this.wtvclient = wtvclient;
this.ssid = this.wtvclient.ssid;
this.unread_mail = this.wtvclient.getSessionData("subscriber_unread_mail") ? this.wtvclient.getSessionData("subscriber_unread_mail") : 0;
this.mailboxes = [
// referenced by id, so order is important!
"Inbox",
"Sent",
"Saved",
this.trashMailboxName
];
this.defaultColors = {
bgcolor: "#171726",
text: "#82A9D9",
link: "#BDA73A",
vlink: "#62B362"
};
}
checkMailIntroSeen() {
return (this.wtvclient.getSessionData("subscriber_mail_intro_seen")) ? this.wtvclient.getSessionData("subscriber_mail_intro_seen") : false;
}
setMailIntroSeen(seen) {
this.wtvclient.setSessionData("subscriber_mail_intro_seen", (seen) ? true : false);
}
mailstoreExists() {
if (!this.isguest) {
if (this.mailstore_dir === null) {
// set mailstore directory local var so we don't call the function every time
var userstore_dir = this.wtvclient.getUserStoreDirectory();
// MailStore
var store_dir = "MailStore" + this.path.sep;
this.mailstore_dir = userstore_dir + store_dir;
}
return this.fs.existsSync(this.mailstore_dir);
}
return null;
}
getSignatureColors(signature = null, sendmail = true) {
var colors = Object.assign({}, this.defaultColors); // start with default colors
if (sendmail) colors.bgcolor = this.sendmailDefaultBGColor;
if (signature) {
if (signature.length > 0) {
if (signature.indexOf('<html>') >= 0) {
if (signature.indexOf('<body') >= 0) {
// parse <body> tag of html signature to get colors
const htmlparser2 = require("htmlparser2");
const dom = htmlparser2.parseDocument(signature);
const body = htmlparser2.DomUtils.getElementsByTagName('body', dom)[0];
if (body.attribs) {
for (const [key, value] of Object.entries(body.attribs)) {
colors[key] = value;
}
}
}
}
}
}
if (!colors.cursor) colors.cursor = colors.link;
return colors;
}
mailboxExists(mailboxid) {
if (mailboxid > this.mailboxes.length) return null;
var mailbox_dir = null;
if (this.mailstoreExists()) {
var mailbox_name = this.getMailboxById(mailboxid);
if (!mailbox_name) return null;
var mailbox_dir = mailbox_name + this.path.sep;
var store_dir = this.mailstore_dir + mailbox_dir;
}
return (store_dir !== null) ? this.fs.existsSync(store_dir) : false;
}
createMailstore() {
if (this.mailstoreExists() === false) {
if (!this.fs.existsSync(this.mailstore_dir)) this.fs.mkdirSync(this.mailstore_dir, { recursive: true });
return true;
}
return false;
}
getMailboxById(mailboxid) {
return (mailboxid < this.mailboxes.length) ? this.mailboxes[mailboxid] : false;
}
getMailboxByName(mailbox_name) {
var mailbox_id = false;
this.mailboxes.every(function (v, k) {
if (v.toLowerCase() == mailbox_name.toLowerCase()) {
mailbox_id = k;
return false;
}
return true;
});
return mailbox_id;
}
getMailboxStoreDir(mailboxid) {
if (this.mailboxExists(mailboxid)) {
var mailbox_name = this.getMailboxById(mailboxid);
return this.mailstore_dir + mailbox_name + this.path.sep;
}
return null;
}
createMailbox(mailboxid) {
var mailbox_exists = this.mailboxExists(mailboxid);
if (mailbox_exists === false) {
var mailbox_name = this.getMailboxById(mailboxid);
var mailbox_dir = mailbox_name + this.path.sep;
var store_dir = this.mailstore_dir + mailbox_dir;
if (!this.fs.existsSync(store_dir)) this.fs.mkdirSync(store_dir, { recursive: true });
return true;
}
return mailbox_exists;
}
createMessageID() {
return this.uuid.v1();
}
createMessage(mailboxid, from_addr, to_addr, msgbody, subject = null, from_name = null, to_name = null, signature = null, date = null, known_sender = false, attachments = [], url = null, url_title = null) {
if (this.createMailbox(mailboxid)) {
if (!date) date = Math.floor(Date.now() / 1000);
var mailbox_path = this.getMailboxStoreDir(mailboxid);
var message_id = this.createMessageID();
var message_file = message_id + this.msgFileExt;
var message_file_out = mailbox_path + message_file;
var message_data = {
"from_addr": from_addr,
"from_name": from_name,
"to_addr": to_addr,
"to_name": to_name,
"date": date,
"subject": subject,
"body": msgbody,
"known_sender": known_sender,
"signature": signature,
"unread": true,
"attachments": attachments,
"url": url,
"url_title": url_title
}
try {
if (this.fs.existsSync(message_file_out)) {
console.log(" * ERROR: Message with this UUID (" + messageid + ") already exists (should never happen). Message lost.");
return false;
}
// encode message into json
var result = this.fs.writeFileSync(message_file_out, JSON.stringify(message_data));
if (!result) return false;
// rely on filesystem times for sorting as it is quicker then reading every file
var file_timestamp = new Date(date * 1000);
fs.utimesSync(message_file, Date.now(), file_timestamp);
if (!result) console.error(" WARNING: Setting timestamp on " + message_file + " failed, mail dates will be inaccurate.");
} catch (e) {
console.error(" # MailErr: Mail Store failed\n", e, "\n", message_file_out, "\n", message_data ,"\n");
}
return false;
}
}
createWelcomeMessage() {
var from_addr = (this.minisrv_config.config.service_owner_account) ? this.minisrv_config.config.service_owner_account : this.minisrv_config.config.service_owner;
from_addr += "@" + this.minisrv_config.config.service_name;
var from_name = this.minisrv_config.config.service_owner
var to_addr = this.wtvclient.getSessionData("subscriber_username") + "@" + this.minisrv_config.config.service_name;
var to_name = this.wtvclient.getSessionData("subscriber_name");
var subj = "Welcome to " + this.minisrv_config.config.service_name;
var msg = "poop";
return this.createMessage(0, from_addr, to_addr, msg, subj, from_name, to_name, null, null, true);
}
getMessage(mailboxid, messageid) {
if (this.createMailbox(mailboxid)) {
var mailbox_path = this.getMailboxStoreDir(mailboxid);
var message_file = messageid + this.msgFileExt;
var message_file_in = mailbox_path + this.path.sep + message_file;
var message_data_raw = null;
if (this.fs.existsSync(message_file_in)) message_data_raw = this.fs.readFileSync(message_file_in);
else console.error(" # MailErr: could not find ", message_file_in);
if (message_data_raw) {
var message_data = JSON.parse(message_data_raw);
message_data.mailbox_path = mailbox_path;
message_data.message_file = message_file;
if (message_data) {
message_data.id = messageid;
// backwards compat
if (!message_data.attachments) message_data.attachments = [];
return message_data;
}
else console.error(" # MailErr: could not parse json in ", message_file_in);
}
}
return false;
}
updateMessage(message_data) {
// encode message into json
var message_out = new Object();
Object.assign(message_out, message_data);
delete message_out.mailbox_path;
delete message_out.message_file;
var result = this.fs.writeFileSync(message_data.mailbox_path + this.path.sep + message_data.message_file, JSON.stringify(message_out));
if (!result) return false;
// rely on filesystem times for sorting as it is quicker then reading every file
var file_timestamp = new Date(message_data.date * 1000);
fs.utimesSync(message_file, Date.now(), file_timestamp);
if (!result) console.error(" WARNING: Setting timestamp on " + message_file + " failed, mail dates will be inaccurate.");
}
checkMessageIdSanity(messageid) {
return /^[A-Za-z0-9\-]{36}$/.test(messageid);
}
listMessages(mailboxid, limit, reverse_sort = false, offset = 0) {
if (this.createMailbox(mailboxid)) {
var mailbox_path = this.getMailboxStoreDir(mailboxid);
var self = this;
var files = this.fs.readdirSync(mailbox_path)
.map(function (v) {
var message_data_raw = null;
var message_date = null;
var message_path = mailbox_path + self.path.sep + v;
if (self.fs.existsSync(message_path)) message_data_raw = self.fs.readFileSync(message_path);
if (message_data_raw) {
var message_data = JSON.parse(message_data_raw);
if (message_data) message_date = message_data.date;
}
var message_date_ret = (message_date) ? message_date : self.fs.statSync(mailbox_path + self.path.sep + v).mtime.getTime();
self.fs.statSync(mailbox_path + self.path.sep + v).mtime.getTime()
return {
name: v,
time: message_date_ret
};
})
.sort(function (a, b) {
if (!reverse_sort) return b.time - a.time;
else return a.time - b.time;
})
.map(function (v) {
if (v.name.substring((v.name.length - self.msgFileExt.length)) === self.msgFileExt) return v.name.substring(0, (v.name.length - 5));
});
if (files.length == 0) return false; // no messages
else {
// todo filter previous results when offset
var messagelist_out = new Array();
Object.keys(files).forEach(function (k) {
var message = self.getMessage(mailboxid, files[k]);
if (message) messagelist_out.push(mailboxid, message);
else console.error(" # MailErr: reading message ID: ", files[k]);
})
return messagelist_out.filter(function (n) { return n; });
}
}
return null; // error
}
countMessages(mailboxid) {
var messages = this.listMessages(mailboxid, 100, false);
var message_count = Object.keys(messages).length;
return (message_count) ? message_count : 0;
}
countUnreadMessages(mailboxid) {
var messages = this.listMessages(mailboxid, 100, false);
var unread = 0;
Object.keys(messages).forEach(function (k) {
if (messages[k].unread) unread++;
});
return unread;
}
getMailboxIcon() {
var icon_image = null;
switch (this.countMessages(0)) {
case 0:
icon_image = "OpenMailbox0.gif";
break;
case 1:
icon_image = "OpenMailbox1.gif";
break;
default:
icon_image = "OpenMailbox2.gif";
break;
}
return icon_image;
}
checkUserExists(username, directory = null) {
// returns the user's ssid, and user_id and userid in an array if true, false if not
var search_dir = this.minisrv_config.config.SessionStore;
var return_val = false;
var self = this;
if (directory) search_dir = directory;
this.fs.readdirSync(search_dir).forEach(file => {
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && !return_val) {
return_val = self.checkUserExists(username, search_dir + self.path.sep + file);
}
if (!file.match(/.*\.json/ig)) return;
try {
var temp_session_data_file = self.fs.readFileSync(search_dir + self.path.sep + file, 'Utf8');
var temp_session_data = JSON.parse(temp_session_data_file);
if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) {
return_val = search_dir.replace(this.minisrv_config.config.SessionStore + self.path.sep, '').replace("user", '').split(self.path.sep);
return_val.push(temp_session_data.subscriber_name);
}
} catch (e) {
console.error(" # Error parsing Session Data JSON", file, e);
}
});
return return_val;
}
getUserMailstore(username) {
var user_data = this.checkUserExists(username);
if (user_data) {
var user_wtvsession = new this.WTVClientSessionData(this.minisrv_config, user_data[0]);
user_wtvsession.user_id = user_data[1];
var user_mailstore = new WTVMail(this.minisrv_config, user_wtvsession)
return user_mailstore;
}
return false;
}
sendMessageToAddr(from_addr, to_addr, msgbody, subject = null, from_name = null, to_name = null, signature = null, attachments = [], url = null, url_title = null) {
if (!to_addr) return "Your message could not be sent.<p>You must specify an addressee in the <blackface>To:</blackface> area.";
if (to_addr.indexOf('@') === -1) to_addr += "@"+this.minisrv_config.config.service_name;
var username = to_addr.split("@")[0];
var dest_minisrv = to_addr.split("@")[1] || this.minisrv_config.config.service_name;
// local only for now
if (dest_minisrv.toLowerCase() !== this.minisrv_config.config.service_name.toLowerCase()) {
return "The m-mail address <strong>" + to_addr + "</strong> is not supported by this MiniSrv.";
}
// find user if local
if (dest_minisrv.toLowerCase() === this.minisrv_config.config.service_name.toLowerCase()) {
var dest_user_mailstore = this.getUserMailstore(username);
// user does not exist
if (!dest_user_mailstore) return "The user <strong>" + username + "</strong> does not exist on MiniSrv <strong>" + dest_minisrv + "</strong>";
if (!to_name) {
var userExistsData = this.checkUserExists(username);
to_name = userExistsData[2];
}
// check if the destination user's Inbox exists yet
if (!dest_user_mailstore.mailboxExists(0)) {
// mailbox does not yet exist, create it
var mailbox_exists = dest_user_mailstore.createMailbox(mailbox);
// Just created Inbox for the first time, so create the welcome message
if (mailbox_exists) dest_user_mailstore.createWelcomeMessage();
}
// if the mailbox exists, deliver the message
if (dest_user_mailstore.mailboxExists(0)) dest_user_mailstore.createMessage(0, from_addr, to_addr, msgbody, subject, from_name, to_name, signature, null, this.isInUserAddressBook(to_addr, from_addr), attachments, url, url_title);
else return "There was an internal error sending the message to <strong>" + to_addr + "</strong>. Please try again later";
// clean up
dest_user_mailstore = null;
return true;
}
return "Unknown error";
}
isInUserAddressBook(address_to_check, address_to_look_for) {
// unimplemented
return false;
}
getMessageMailboxName(messageid) {
// returns the mailbox id of which the message was found for the current user
var self = this;
var mailbox_name = false;
if (this.checkMessageIdSanity(messageid)) {
if (this.mailstoreExists()) {
this.fs.readdirSync(this.mailstore_dir).every(mailbox => {
if (mailbox_name) return false;
self.fs.readdirSync(self.mailstore_dir + mailbox).every(file => {
var regexSearch = messageid + self.msgFileExt;
var re = new RegExp(regexSearch, "ig");
if (!file.match(re)) return true;
mailbox_name = mailbox;
return false;
});
return true;
});
}
}
return mailbox_name;
}
getMessageMailboxID(messageid) {
var mailbox_name = this.getMessageMailboxName(messageid);
if (!mailbox_name) return false;
return this.getMailboxByName(mailbox_name);
}
getMessageByID(messageid) {
var mailbox_name = this.getMessageMailboxName(messageid);
if (!mailbox_name) return false;
var mailboxid = this.mailboxes.findIndex((value) => value == mailbox_name);
if (mailboxid !== false) return this.getMessage(mailboxid, messageid);
return null;
}
moveMailMessage(messageid, dest_mailbox_id) {
// returns true if successful, false if failed.
var currentMailbox = this.getMessageMailboxID(messageid);
// Same mailbox
if (dest_mailbox_id == currentMailbox) return false;
// Invalid destination mailbox ID
if (dest_mailbox_id > (this.mailboxes.length - 1) || dest_mailbox_id < 0) return false;
if (!this.mailboxExists(dest_mailbox_id)) this.createMailbox(dest_mailbox_id);
var currentMailStoreDir = this.getMailboxStoreDir(currentMailbox);
if (!currentMailStoreDir) return false;
var destMailStoreDir = this.getMailboxStoreDir(dest_mailbox_id);
if (!destMailStoreDir) return false;
var currentMailFile = currentMailStoreDir + this.path.sep + messageid + this.msgFileExt;
var destMailFile = destMailStoreDir + this.path.sep + messageid + this.msgFileExt;
// File exists
if (this.fs.existsSync(destMailFile)) return false;
return this.fs.renameSync(currentMailFile, destMailFile);
}
deleteMessage(messageid) {
var currentMailbox = this.getMessageMailboxName(messageid);
var trashMailbox = this.getMailboxByName(this.trashMailboxName);
if (currentMailbox != trashMailbox) {
// if not in the trash, move it to trash
return this.moveMailMessage(messageid, trashMailbox);
} else {
// if its already in the trash, delete it forever
var currentMailFile = this.getMailboxStoreDir(trashMailbox) + this.path.sep + messageid + this.msgFileExt;
if (this.fs.fileExistsSync(currentMailFile))
return this.fs.unlink(currentMailFile);
else
return false;
}
}
setMessageReadStatus(messageid, read = true) {
var message = this.getMessageByID(messageid);
if (!message) return false;
message.unread = !read;
this.updateMessage(message);
return true;
}
}
module.exports = WTVMail;

View File

@@ -0,0 +1,205 @@
/**
* 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 isOldBuild = this.wtvshared.isOldBuild(ssid_session);
var is_softmodem = false;
if (ssid_session.get("wtv-client-rom-type")) is_softmodem = ssid_session.get("wtv-client-rom-type").match(/softmodem/);
if (!is_bf0app && ((!is_softmodem && !isOldBuild) || (is_softmodem && !isOldBuild))) {
// 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|midi|wav|wave|aif(f)?)$/)) compress_data = true; // s3m, mod, xm, midi & wav
if (content_type.match(/^application\/karaoke$/)) compress_data = true; // midi karaoke
if (content_type.match(/^binary\/(x-wtv-approm|doom-data)/)) compress_data = true; // approms and DOOM WADs
if (content_type.match(/^wtv\/download-list$/)) compress_data = true; // WebTV Download List
}
}
}
}
}
// 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 "kar":
wtv_mime_type = "audio/midi";
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 "kar":
wtv_mime_type = "application/karaoke";
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 (modern_mime_type === false) modern_mime_type = "application/octet-stream";
if (wtv_mime_type == "") wtv_mime_type = modern_mime_type;
return new Array(wtv_mime_type, modern_mime_type);
}
}
module.exports = WTVMime;

View File

@@ -0,0 +1,5 @@
class WTVNews {
}
module.exports = WTVNews;

View File

@@ -0,0 +1,184 @@
class WTVRegister {
fs = require('fs');
path = require('path');
minisrv_config = [];
service_owner = "a minisrv user";
session_store_dir = null;
constructor(minisrv_config, session_store_dir = null) {
this.minisrv_config = minisrv_config;
this.service_owner = minisrv_config.config.service_owner || "a minisrv user";
if (session_store_dir) this.session_store_dir = session_store_dir
}
getServiceOperator(first_letter_lower = false) {
if (this.service_owner == "a minisrv user") {
if (first_letter_lower) return "the operator of this service";
else return "The operator of this service";
} else {
return this.service_owner;
}
}
checkUsernameSanity(username) {
var regex_str = "^([A-Za-z0-9\-\_]{" + this.minisrv_config.config.user_accounts.min_username_length + "," + this.minisrv_config.config.user_accounts.max_username_length + "})$";
var regex = new RegExp(regex_str);
console.log(username, username.length, regex.test(username));
return regex.test(username);
}
checkUsernameAvailable(username, directory = null) {
// returns the user's ssid, and user_id and userid in an array if true, false if not
var search_dir = this.minisrv_config.config.SessionStore;
var return_val = false;
var self = this;
if (directory) search_dir = directory;
this.fs.readdirSync(search_dir).forEach(file => {
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && !return_val) {
return_val = self.checkUsernameAvailable(username, search_dir + self.path.sep + file);
}
if (!file.match(/.*\.json/ig)) return;
try {
var temp_session_data_file = self.fs.readFileSync(search_dir + self.path.sep + file, 'Utf8');
var temp_session_data = JSON.parse(temp_session_data_file);
if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) {
return_val = true;
}
} catch (e) {
console.error(" # Error parsing Session Data JSON", search_dir + self.path.sep + file, e);
}
});
return !return_val;
}
/**
* Generations regnstration template
* @param {string} title HTML Page Title
* @param {string} main_content Main center content
* @param {string} form_buttons Form and buttons
* @param {boolean} is_old_build True or false
* @returns {string} HTML Page
*/
getHTMLTemplate(title, main_content, form_buttons, is_old_build) {
var data;
if (is_old_build) {
data = `<html>
<head>
<title>
${title}
</title>
<display nooptions>
</head>
<body bgcolor=#191919 text=#42CC55 fontsize=large hspace=0 vspace=0>
<table cellspacing=0 cellpadding=0>
<tr>
<td width=104 height=74 valign=middle align=center bgcolor=#3B3A4D>
<img src="${this.minisrv_config.config.service_logo}" width=86 height=64>
<td width=20 valign=top align=left bgcolor=#3B3A4D>
<spacer>
<td colspan=2 width=100% align=left bgcolor=#3B3A4D>
<font color=D6DFD0 size=+2>
<blackface>
<shadow>
<spacer type=block width=1 height=4>
<br>
${title}
</shadow>
</blackface>
</font>
</tr>
</td>
</table>
<table width=520 align=center cellspacing=0 cellpadding=0>
<tr>
<td height=272>
<font size=+1>
${main_content}
<p>
</font>
</td>
</tr>
</table>
<hr>
<p>
<table align=right cellspacing=0 cellpadding=0>
<tr>
<td>
<spacer type=block height=10>
${form_buttons}
</td>
<td>
&nbsp; &nbsp;
</td>
</tr>
</table>
</body>
</html>
`;
} else {
data = `<html>
<head>
<title>
${title}
</title>
<display nooptions noscroll NoScroll>
</head>
<body noscroll
bgcolor="#171726" text="#D1D3D3" link=#FFEA9C vlink=#FFEA9C
hspace=0 vspace=0 fontsize="large"
>
<table cellspacing=0 cellpadding=0 border=0 width=560 bgcolor=#171726>
<tr>
<td align=middle bgcolor="#5b6c81" border=0 colspan= 3 width="100" height="80">
<img src="${this.minisrv_config.config.service_logo}" WIDTH="87" HEIGHT="67">
<td colspan= 6 bgcolor="#5b6c81" border=0 width=100% absheight="80" valign=bottom >
<img src="images/head_registration.gif" >
<tr>
<td bgcolor="#5b6c81" border=0 rowspan=2 width=21 height= 220></td>
<td bgcolor="#171726" border=0 width=9 height=25 align=left valign=top>
<img src="images/L_corner.gif" width=8 height=8>
<td bgcolor="#171726" border=1 colspan=1 width=70 height=25>
<td colspan=6 bgcolor="#171726" border=1 height=25 align=left valign=bottom gradcolor=#262E3D gradangle=90>
<font color=#d1d3d3 size=+1>
<blackface>
${title}
</blackface></font>
<tr> <td border=0 width=40 bgcolor="#171726" rowspan="2" >
<td absheight=20 width=100 bgcolor="#171726" colspan=6>
</tr>
</table>
<table cellspacing=0 cellpadding=0 border=0 width=560 bgcolor=#171726>
<tr>
<td bgcolor= "#5b6c81" border=0 rowspan=6 abswidth=21 height= 220></td>
<td border=0 abswidth=40 bgcolor="#171726" rowspan="6" >
<td height=230 width= 300 bgcolor="#171726" colspan=5 valign=top align=left>
${main_content}
<td abswidth=20 bgcolor=#171726 >
</tr>
<tr>
<td valign= bottom height=15 colspan=7 bgcolor=#171726>
<shadow>
<hr size=5 valign=bottom></shadow>
</tr>
<tr>
<td border=2 colspan=4 width=100 height=50 bgcolor=#171726 valign=top align=left>
<font size=-1><i>
</i></font>
<td bgcolor=#171726 height=50 width=560 valign=top align=right>
<font size=-1 color=#e7ce4a>
${form_buttons}
<td abswidth=13 absheight=50 bgcolor=#171726>
</tr>
</table>
</body>
</html>`;
}
return data;
}
}
module.exports = WTVRegister;

View File

@@ -0,0 +1,396 @@
const CryptoJS = require('crypto-js');
const endianness = require('endianness');
var crypto = require('crypto');
/**
* Javascript implementation of WTVP Security
*
* Special Thanks to eMac (Eric MacDonald)
* For the encryption/decryption information and process
*
* By: zefie
*/
class WTVSec {
// 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;
challenge_signed_key = null;
challenge_raw = null;
challenge_response = null;
ticket_b64 = null;
incarnation = 0;
session_key1 = null;
session_key2 = null;
hRC4_Key1 = null;
hRC4_Key2 = null;
RC4Session = new Array();
minisrv_config = [];
update_ticket = false;
ticket_store = {};
/**
*
* Initialize the WTVSec class.
*
* @param {Number} wtv_incarnation Sets the wtv-incarnation for this instance
* @param {Boolean} minisrv_config.config.debug_flags.debug Enable debugging
*
*/
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) {
this.incarnation = wtv_incarnation;
this.current_shared_key = this.initial_shared_key;
} else {
throw ("Invalid initial key length");
}
}
/**
* Set the wtv-incarnation for this instance
*
* @param {Number} wtv_incarnation
*/
set_incarnation(wtv_incarnation) {
if (this.incarnation != wtv_incarnation) {
this.incarnation = wtv_incarnation;
this.SecureOn();
}
}
/**
* Increments the wtv-incaration for this instance by 1
*/
increment_incarnation() {
this.set_incarnation(parseInt(this.incarnation) + 1);
}
/**
* Clones a WordArray to allow modification without referencing its original
* @param {CryptoJS.lib.WordArray} wa
*
* @returns {CryptoJS.lib.WordArray}
*/
DuplicateWordArray(wa) {
return CryptoJS.lib.WordArray.create(this.wordArrayToBuffer(wa));
}
/**
* Prepares the wtv-ticket for this instance
*/
PrepareTicket() {
// store last challenge response in ticket
if (this.minisrv_config.config.debug_flags.debug) console.log(" * Preparing a new ticket with ticket_store:", this.ticket_store)
var ticket_data_raw = this.challenge_raw;
try {
var ticket_data = ticket_data_raw.toString(CryptoJS.enc.Hex) + CryptoJS.enc.Utf8.parse(JSON.stringify(this.ticket_store)).toString(CryptoJS.enc.Hex);
ticket_data_raw = CryptoJS.enc.Hex.parse(ticket_data);
var ticket_data_enc = CryptoJS.DES.encrypt(ticket_data_raw, this.initial_shared_key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
});
// create a copy of WordArray since concat modifies the original
var challenge_signed_key = this.DuplicateWordArray(this.challenge_signed_key);
this.ticket_b64 = challenge_signed_key.concat(ticket_data_enc.ciphertext).toString(CryptoJS.enc.Base64);
} catch (e) {
console.log("Error encrypting ticket: " + e.toString());
return null;
}
return this.ticket_b64;
}
tryDecodeJSON(json_string) {
var out;
try {
out = JSON.parse(json_string);
} catch (e) {
console.log(e);
out = {};
}
return out;
}
/**
* Decodes a wtv-ticket to set up this instance
*
* @param {Base64} ticket_b64
*/
DecodeTicket(ticket_b64) {
var ticket_hex = CryptoJS.enc.Base64.parse(ticket_b64).toString(CryptoJS.enc.Hex);
var challenge_key = CryptoJS.enc.Hex.parse(ticket_hex.substring(0, 16));
var challenge_enc = CryptoJS.enc.Hex.parse(ticket_hex.substring(16));
var ticket_dec = CryptoJS.DES.decrypt(
{
ciphertext: challenge_enc
},
this.initial_shared_key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
);
var data_offset = 216; // (108 * 2);
var challenge_code = ticket_dec.toString().substring(0, data_offset);
var challenge_code_b64 = CryptoJS.enc.Hex.parse(challenge_code).toString(CryptoJS.enc.Base64);
if ((ticket_dec.sigBytes * 2) >= challenge_code.length) {
var ticket_data_dec = CryptoJS.enc.Hex.parse(ticket_dec.toString().substring(data_offset)).toString(CryptoJS.enc.Utf8);
this.ticket_store = this.tryDecodeJSON(ticket_data_dec);
} else {
this.ticket_store = {};
}
this.ProcessChallenge(challenge_code_b64, challenge_key);
if (this.minisrv_config.config.debug_flags.debug) console.log(" * Decoded session from wtv-ticket with ticket_store:", this.ticket_store);
}
getTicketData(key = null) {
if (typeof (this.ticket_store) === 'session_store') return null;
else if (key === null) return this.ticket_store;
else return null;
}
setTicketData(key, value) {
if (key === null) throw ("WTVSec.ssetTicketDataet(): invalid key provided");
if (typeof (this.ticket_store) === 'undefined') this.ticket_store = {};
this.ticket_store[key] = value;
if (this.ticket_b64) this.PrepareTicket();
this.update_ticket = true;
}
deleteTicketData(key) {
if (key === null) throw ("WTVSec.deleteTicketData(): invalid key provided");
if (typeof (this.ticket_store) === 'undefined') {
this.ticket_store = {};
return;
}
delete this.ticket_store[key];
if (this.ticket_b64) this.PrepareTicket();
this.update_ticket = true;
}
/**
* Processes a wtv-challenge to get the expected response
* @param {Base64} wtv_challenge
* @param {any} key
*
* @returns {CryptoJS.lib.WordArray} wtv-challenge-response (or blank if failed)
*/
ProcessChallenge(wtv_challenge, key = this.current_shared_key) {
var challenge_raw = CryptoJS.enc.Base64.parse(wtv_challenge);
if (challenge_raw.sigBytes > 8) {
var challenge_raw_hex = challenge_raw.toString(CryptoJS.enc.Hex);
var challenge_id_hex = challenge_raw_hex.substring(0, (8 * 2));
var challenge_enc_hex = challenge_raw_hex.substring((8*2));
var challenge_enc = CryptoJS.enc.Hex.parse(challenge_enc_hex);
var challenge_decrypted = CryptoJS.DES.decrypt(
{
ciphertext: challenge_enc
},
key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
}
);
var challenge_dec_hex = challenge_decrypted.toString(CryptoJS.enc.Hex);
var challenge_md5_challenge = CryptoJS.MD5(CryptoJS.enc.Hex.parse(challenge_dec_hex.substring(0, (80 * 2))));
var test = challenge_dec_hex.substring((80 * 2), (96 * 2));
var test2 = challenge_md5_challenge.toString(CryptoJS.enc.Hex);
if (test == test2) {
this.current_shared_key = CryptoJS.enc.Hex.parse(challenge_dec_hex.substring((72*2), (80*2)));
var challenge_echo = CryptoJS.enc.Hex.parse(challenge_dec_hex.substr(0, (40*2)));
// RC4 encryption keys.Stored in the wtv-ticket on the server side.
this.session_key1 = CryptoJS.enc.Hex.parse(challenge_dec_hex.substring((40*2), (56*2)));
this.session_key2 = CryptoJS.enc.Hex.parse(challenge_dec_hex.substring((56*2), (72*2)));
var echo_encrypted = CryptoJS.DES.encrypt(CryptoJS.MD5(challenge_echo).concat(challenge_echo).concat(CryptoJS.enc.Utf8.parse("\x08".repeat(8))), this.current_shared_key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
// Last bytes is just extra padding
this.challenge_raw = challenge_raw;
this.challenge_key = this.current_shared_key;
var challenge_response = CryptoJS.enc.Hex.parse(challenge_raw_hex.substr(0, (8 * 2))).concat(echo_encrypted.ciphertext);
return challenge_response;
} else {
return "";
}
} else {
throw ("Invalid challenge length");
}
}
/**
* Generates a wtv-challenge for this instance
*
* @returns {Base64} wtv-challenge
*/
IssueChallenge() {
/*
* bytes 0-8: Random id? Just echoed in the response
* bytes 8 - XX: DES encrypted block.Encrypted with the initial key or subsequent keys from the challenge.
* bytes 8 - 48: hidden random data we echo back in the response
* bytes 48 - 64: session key 1 used in RC4 encryption triggered by SECURE ON
* bytes 64 - 80: session key 2 used in RC4 encryption triggered by SECURE ON
* bytes 80 - 88: new key for future challenges
* bytes 88 - 104: MD5 of 8 - 88
* bytes 104 - 112: padding.not important
*/
var challenge_id = CryptoJS.lib.WordArray.random(8);
var echo_me = CryptoJS.lib.WordArray.random(40);
this.session_key1 = CryptoJS.lib.WordArray.random(16);
this.session_key2 = CryptoJS.lib.WordArray.random(16);
var new_shared_key = CryptoJS.lib.WordArray.random(8);
var session_key1 = this.DuplicateWordArray(this.session_key1);
var session_key2 = this.DuplicateWordArray(this.session_key2);
var challenge_puzzle = echo_me.concat(session_key1.concat(session_key2.concat(new_shared_key)));
var challenge_secret = challenge_puzzle.concat(CryptoJS.MD5(challenge_puzzle).concat(CryptoJS.enc.Hex.parse("\x08".repeat(8))));
// Shhhh!!
var challenge_secreted = CryptoJS.DES.encrypt(challenge_secret, this.current_shared_key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
var challenge = challenge_id.concat(challenge_secreted.ciphertext);
var challenge_b64 = challenge.toString(CryptoJS.enc.Base64);
// get the expected response for when client sends it
this.challenge_signed_key = this.current_shared_key;
this.challenge_response = this.ProcessChallenge(challenge_b64);
return challenge_b64;
}
/**
* convert a CryptoJS.lib.WordArray to a Javascript Buffer
* @param {CryptoJS.lib.WordArray} wordArray
*
* #returns {Buffer} JS Buffer object
*/
wordArrayToBuffer(wordArray) {
if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex');
else return null;
}
/**
* Starts an encryption session
* @param {Number} rc4session Session Type (0 = enc k1, 1 = dec k1, 3 = enc k2, 4 = dec k2, default: all)
*
*/
SecureOn(rc4session = null) {
if (this.minisrv_config.config.debug_flags.debug) console.log(" # Generating RC4 sessions with wtv-incarnation: " + this.incarnation);
var buf = new Uint8Array([0xff & this.incarnation, 0xff & (this.incarnation >> 8), 0xff & (this.incarnation >> 16), 0xff & (this.incarnation >> 24)]);
endianness(buf, 4);
this.hRC4_Key1 = CryptoJS.MD5(this.DuplicateWordArray(this.session_key1).concat(CryptoJS.lib.WordArray.create(buf).concat(this.DuplicateWordArray(this.session_key1))));
this.hRC4_Key2 = CryptoJS.MD5(this.DuplicateWordArray(this.session_key2).concat(CryptoJS.lib.WordArray.create(buf).concat(this.DuplicateWordArray(this.session_key2))));
var key1 = this.wordArrayToBuffer(this.hRC4_Key1);
var key2 = this.wordArrayToBuffer(this.hRC4_Key2);
switch (rc4session) {
case 0:
this.RC4Session[0] = crypto.createCipheriv('rc4', key1,'');
break;
case 1:
this.RC4Session[1] = crypto.createDecipheriv('rc4', key1,'');
break;
case 2:
this.RC4Session[2] = crypto.createCipheriv('rc4', key2,'');
break;
case 3:
this.RC4Session[3] = crypto.createDecipheriv('rc4', key2,'');
break;
default:
this.RC4Session[0] = crypto.createCipheriv('rc4', key1, '');
this.RC4Session[1] = crypto.createDecipheriv('rc4', key1, '');
this.RC4Session[2] = crypto.createCipheriv('rc4', key2, '');
this.RC4Session[3] = crypto.createDecipheriv('rc4', key2, '');
break;
}
}
/**
* RC4 Encrypt data
* @param {Number} keynum Which key to use (0 = k1, 1 = k2)
* @param {CryptoJS.lib.WordArray|ArrayBuffer|Buffer} data Data to encrypt
*
* @returns {ArrayBuffer} Encrypted data
*/
Encrypt(keynum, data) {
var session_id;
switch (keynum) {
case 0:
session_id = 0;
break;
case 1:
session_id = 2
break;
default:
throw ("Invalid key option (0 or 1 only)");
break;
}
if (!this.RC4Session[session_id]) {
this.SecureOn(session_id);
}
if (data.words) {
data = this.wordArrayToBuffer(data);
} else if (data.constructor === ArrayBuffer) {
data = new Buffer.from(data);
}
return this.RC4Session[session_id].update(data);
}
/**
* RC4 Decrypt data
* @param {Number} keynum Which key to use (0 = k1, 1 = k2)
* @param {CryptoJS.lib.WordArray|ArrayBuffer|Buffer} data Data to decrypt
*
* @returns {ArrayBuffer} Decrypted data
*/
Decrypt(keynum, data) {
var session_id;
switch (keynum) {
case 0:
session_id = 1;
break;
case 1:
session_id = 3;
break;
default:
throw ("Invalid key option (0 or 1 only)");
break;
}
if (!this.RC4Session[session_id]) {
this.SecureOn(session_id);
}
if (data.words) {
data = this.wordArrayToBuffer(data);
} else if (data.constructor === ArrayBuffer) {
data = new Buffer.from(data);
}
return this.RC4Session[session_id].update(data);
}
}
module.exports = WTVSec;

View File

@@ -0,0 +1,692 @@
/**
* Shared functions across all classes and apps
*/
class WTVShared {
path = require('path');
fs = require('fs');
v8 = require('v8');
zlib = require('zlib');
CryptoJS = require('crypto-js');
html_entities = require('html-entities'); // used externally by service scripts
sanitizeHtml = require('sanitize-html');
iconv = require('iconv-lite');
parentDirectory = process.cwd()
minisrv_config = [];
constructor(minisrv_config, quiet = false) {
if (minisrv_config == null) this.minisrv_config = this.readMiniSrvConfig(true, !quiet);
else 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;
}
}
if (!String.prototype.hexEncode) {
String.prototype.hexEncode = function () {
var result = '';
for (var i = 0; i < this.length; i++) {
result += this.charCodeAt(i).toString(16);
}
return result;
}
}
}
getServiceString(service, overrides = {}) {
// used externally by service scripts
if (service === "all") {
var out = "";
Object.keys(minisrv_config.services).forEach(function (k) {
if (overrides.exceptions) {
Object.keys(overrides.exceptions).forEach(function (j) {
if (k != overrides.exceptions[j]) out += minisrv_config.services[k].toString(overrides) + "\n";
});
} else {
out += minisrv_config.services[k].toString(overrides) + "\n";
}
});
return out;
} else {
if (!minisrv_config.services[service]) {
throw ("SERVICE ERROR: Attempted to provision unconfigured service: " + service)
} else {
return minisrv_config.services[service].toString(overrides);
}
}
}
parseBool(val) {
if (typeof val === 'string')
val = val.toLowerCase();
return val === true || val === "true" || val === 1;
}
getQueryString(query) {
// for easy retrofitting old code to work with the webtvism of allowing multiple of the same query name
// pass it the query, and it will return a string regardless. if its a string it just sends it back
// if its an array we just pull the last object
if (typeof (query) === "object")
return query[(Object.keys(query).length - 1)];
else
return query
}
htmlEntitize(string, process_newline = false) {
string = this.html_entities.encode(string).replace(/&apos;/g, "'");
if (process_newline) string = string.replace(/\n/gi, "<br>").replace(/\r/gi, "");
return string;
}
sanitizeSignature(string) {
var allowedSchemes = ['http', 'https', 'ftp', 'mailto'];
var self = this;
Object.keys(this.minisrv_config.services).forEach(function (k) {
var flags = self.minisrv_config.services[k].flags;
if (flags) {
if (flags == "0x00000004" || flags == "0x00000007") {
allowedSchemes.push(self.minisrv_config.services[k].name);
}
}
});
const clean = this.sanitizeHtml(string, {
allowedTags: ['a', 'audioscope', 'b', 'bgsound', 'big', 'blackface', 'blockquote', 'bq', 'br', 'caption', 'center', 'cite', 'c', 'dd', 'dfn', 'div', 'dl', 'dt', 'fn', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'html', 'i', 'img', 'label', 'li', 'link', 'listing', 'em', 'marquee', 'nobr', 'note', 'ol', 'p', 'plaintext', 'pre', 's', 'samp', 'small', 'span', 'strike', 'strong', 'style', 'sub', 'sup', 'tbody', 'table', 'td', 'th', 'tr', 'tt', 'u', 'ul'],
disallowedTagsMode: 'discard',
allowedAttributes: {
a: ['href', 'name', 'target'],
img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
bgsound: ['src', 'loop'],
font: ['size', 'name', 'color'],
marquee: ['speed'],
},
allowedSchemes: allowedSchemes,
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowVulnerableTags: true,
allowProtocolRelative: false
})
// todo: add missing user open tags (eg </i> if user did not close it) (might be done by sanitize-html?)
// todo: figure out bgcolor and text color voodoo
return clean;
}
isASCII(str) {
for (var i = 0, strLen = str.length; i < strLen; ++i) {
if (str.charCodeAt(i) > 127) return false;
}
return true;
}
isHTML(str) {
return /<[a-z][\s\S]*>/i.test(str);
}
utf8Decode(utf8String) {
if (typeof utf8String != 'string') throw new TypeError('parameter <20>utf8String<6E> is not a string');
// note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
const unicodeString = utf8String.replace(
/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars
function (c) { // (note parentheses for precedence)
var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f);
return String.fromCharCode(cc);
}
).replace(
/[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars
function (c) { // (note parentheses for precedence)
var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f;
return String.fromCharCode(cc);
}
);
return unicodeString;
}
decodeBufferText(buf) {
var out = "";
out = this.utf8Decode(this.iconv.decode(Buffer.from(buf),'ISO-8859-1'));
return out;
}
returnAbsolutePath(check_path) {
if (check_path.substring(0, 1) != this.path.sep && check_path.substring(1, 1) != ":") {
// non-absolute path, so use current directory as base
check_path = this.parentDirectory + this.path.sep + check_path;
} else {
// already absolute path
}
return check_path;
}
isMiniBrowser(ssid_session) {
return (ssid_session.get("wtv-need-upgrade") || ssid_session.get("wtv-used-8675309")) ? true : false;
}
isOldBuild(ssid_session) {
return (this.isMiniBrowser(ssid_session) || parseInt(ssid_session.get("wtv-system-version")) < 3500) ? true : false;
}
getUserConfig() {
try {
var user_config_filename = this.getAbsolutePath("user_config.json", this.parentDirectory);
if (this.fs.lstatSync(user_config_filename)) {
try {
var minisrv_user_config = JSON.parse(this.fs.readFileSync(user_config_filename));
} catch (e) {
console.error("ERROR: Could not read user_config.json", e);
var throw_me = true;
}
} else {
var minisrv_user_config = {}
}
return minisrv_user_config;
} catch (e) {
if (minisrv_config.config.debug_flags) {
if (minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration.");
}
}
}
readMiniSrvConfig(user_config = true, notices = true, reload_notice = false) {
if (notices || reload_notice) console.log(" *** Reading global configuration...");
try {
var minisrv_config = JSON.parse(this.fs.readFileSync(this.getAbsolutePath("config.json", __dirname)));
} catch (e) {
throw ("ERROR: Could not read config.json", e);
}
var integrateConfig = function(main, user) {
Object.keys(user).forEach(function (k) {
if (typeof (user[k]) == 'object' && user[k] != null) {
// new entry
if (!main[k]) main[k] = new Array();
// go down the rabbit hole
main[k] = integrateConfig(main[k], user[k]);
} else {
// update main config
main[k] = user[k];
}
});
return main;
}
if (user_config) {
try {
if (notices || reload_notice) console.log(" *** Reading user configuration...");
var minisrv_user_config = this.getUserConfig()
if (!minisrv_user_config) throw "ERROR: Could not read user_config.json";
try {
minisrv_config = integrateConfig(minisrv_config, minisrv_user_config)
} catch (e) {
console.error("ERROR: Could not read user_config.json", e);
}
} catch (e) {
if (minisrv_config.config.debug_flags) {
if (minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration.");
}
}
}
// defaults
minisrv_config.config.debug_flags = [];
minisrv_config.config.debug_flags.debug = false;
minisrv_config.config.debug_flags.quiet = true; // will squash minisrv_config.config.debug_flags.debug even if its true
minisrv_config.config.debug_flags.show_headers = false;
if (minisrv_config.config.verbosity) {
switch (minisrv_config.config.verbosity) {
case 0:
minisrv_config.config.debug_flags.debug = false;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = false;
if (notices) console.log(" * Console Verbosity level 0 (quietest)")
break;
case 1:
minisrv_config.config.debug_flags.debug = false;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = true;
if (notices) console.log(" * Console Verbosity level 1 (headers shown)")
break;
case 2:
minisrv_config.config.debug_flags.debug = true;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = false;
if (notices) console.log(" * Console Verbosity level 2 (verbose without headers)")
break;
case 3:
minisrv_config.config.debug_flags.debug = true;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = true;
if (notices) console.log(" * Console Verbosity level 3 (verbose with headers)")
break;
default:
minisrv_config.config.debug_flags.debug = true;
minisrv_config.config.debug_flags.quiet = false;
minisrv_config.config.debug_flags.show_headers = true;
if (notices) console.log(" * Console Verbosity level 4 (debug verbosity)")
break;
}
}
if (notices || reload_notice) console.log(" *** Configuration successfully read.");
this.minisrv_config = minisrv_config;
return this.minisrv_config;
}
writeToUserConfig(config) {
if (config) {
try {
var minisrv_user_config = this.getUserConfig();
// write back
try {
var new_user_config = {};
Object.assign(new_user_config, minisrv_user_config, config);
if (this.minisrv_config.config.debug_flags.debug) console.log(" * Writing new user configuration...");
this.fs.writeFileSync(this.getAbsolutePath("user_config.json", parentDirectory), JSON.stringify(new_user_config, null, "\t"));
}
catch (e) {
if (this.minisrv_config.config.debug_flags) {
if (this.minisrv_config.config.debug_flags.debug) console.error(" * WARNING: Could not update user config. Data may have been lost.", e);
}
}
} catch (e) {
if (this.minisrv_config.config.debug_flags) {
if (this.minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration.");
}
}
}
}
getMiniSrvConfig() {
return this.minisrv_config;
}
/**
* 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();
}
urlEncodeBytes = (buf) => {
let encoded = ''
for (let i = 0; i < buf.length; i++) {
const charBuf = Buffer.from('00', 'hex')
charBuf.writeUInt8(buf[i])
const char = charBuf.toString()
// if the character is safe, then just print it, otherwise encode
if (isUrlSafe(char)) {
encoded += char
} else {
encoded += `%${charBuf.toString('hex').toUpperCase()}`
}
}
return encoded
}
urlDecodeBytes = (encoded) => {
let decoded = Buffer.from('')
for (let i = 0; i < encoded.length; i++) {
if (encoded[i] === '%') {
const charBuf = Buffer.from(`${encoded[i + 1]}${encoded[i + 2]}`, 'hex')
decoded = Buffer.concat([decoded, charBuf])
i += 2
} else {
const charBuf = Buffer.from(encoded[i])
decoded = Buffer.concat([decoded, charBuf])
}
}
return decoded
}
/**
* 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 {
var newobj = Object.assign({}, obj);
if (obj.post_data) newobj.post_data = obj.post_data;
if (newobj["wtv-client-serial-number"]) {
var ssid = newobj["wtv-client-serial-number"];
if (ssid.substr(0, 8) == "MSTVSIMU") {
newobj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20);
} else if (ssid.substr(0, 5) == "1SEGA") {
newobj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13);
} else {
newobj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9);
}
}
return newobj;
}
} else {
return obj;
}
}
filterRequestLog(obj) {
if (this.minisrv_config.config.filter_passwords_in_logs === true) {
if (obj.query) {
var newobj = Object.assign({}, obj);
if (obj.post_data) newobj.post_data = obj.post_data;
Object.keys(newobj.query).forEach(function (k) {
var key = k.toLowerCase();
switch (true) {
case /passw(or)?d/.test(key):
case /^pass$/.test(key):
newobj.query[key] = ('*').repeat(newobj.query[key].length);
break;
}
});
return newobj;
}
}
return obj;
}
decodePostData(obj) {
if (obj.post_data) {
if (this.minisrv_config.config.filter_passwords_in_logs === true) {
// complex, to filter
var post_obj = {};
post_obj.query = [];
try {
var post_text = obj.post_data.toString(this.CryptoJS.enc.Utf8);
if (post_text.length > 0) {
post_text = post_text.split("&");
for (let i = 0; i < post_text.length; i++) {
var qraw_split = post_text[i].split("=");
if (qraw_split.length == 2) {
var k = qraw_split[0];
post_obj.query[k] = unescape(post_text[i].split("=")[1].replace(/\+/g, "%20"));
}
}
}
var post_obj = this.filterRequestLog(post_obj);
post_text = "";
Object.keys(post_obj.query).forEach(function (k) {
post_text += k + "=" + post_obj.query[k] + "&";
});
post_text = post_text.substring(0, post_text.length - 1);
obj.post_data = post_text.hexEncode();
} catch (e) {
obj.post_data = obj.post_data.toString(this.CryptoJS.enc.Hex);
}
} else {
// simple, no filter
obj.post_data = obj.post_data.toString();
}
}
return obj;
}
unloadModule(moduleName) {
// for handling template classes
var solvedName = require.resolve(moduleName),
nodeModule = require.cache[solvedName];
if (nodeModule) {
for (var i = 0; i < nodeModule.children.length; i++) {
var child = nodeModule.children[i];
this.unloadModule(child.filename);
}
delete require.cache[solvedName];
}
}
/**
* Returns an absolute path
* @param {string} path
* @param {string} directory Root directory
*/
getAbsolutePath(path, directory = null) {
if (directory) {
if (path.indexOf(directory) == -1) {
directory = this.getAbsolutePath(directory);
try {
if (this.fs.lstatSync(directory).isDirectory()) directory = directory + this.path.sep;
} catch (e) { }
path = directory + path;
}
}
return this.fixPathSlashes(this.path.resolve(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
*/
getFilePath(path) {
var path_split = path.split('/');
path_split.pop();
return path_split.join('/');
}
/**
* Gets the file extension from a path
* @param {string} path
* @returns {String} File Extension (without dot)
*/
getFileExt(path) {
return path.reverse().split(".")[0].reverse();
}
getLineFromFile(filename, line_no, callback) {
var stream = this.fs.createReadStream(filename, {
flags: 'r',
encoding: 'utf-8',
fd: null,
bufferSize: 64 * 1024
});
var fileData = '';
stream.on('data', function (data) {
fileData += data;
// The next lines should be improved
var lines = fileData.split("\n");
if (lines.length >= +line_no) {
stream.destroy();
callback(null, lines[+line_no]);
}
});
stream.on('error', function () {
callback('Error', null);
});
stream.on('end', function () {
callback('File end reached without finding line', null);
});
}
doErrorPage(code, data = null, pc_mode = false) {
var headers = null;
switch (code) {
case 404:
if (data === null) data = "The service could not find the requested page.";
if (pc_mode) headers = "404 Not Found\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
case 400:
case 500:
if (data === null) data = this.minisrv_config.config.service_name + " ran into a technical problem.";
if (pc_mode) headers = "500 Internal Server Error\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
case 401:
if (data === null) data = "Access Denied.";
if (pc_mode) headers = "401 Access Denied\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
default:
headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
}
console.error(" * doErrorPage Called:", code, data);
return new Array(headers, data);
}
/**
* Strips bad things from paths
* @param {string} base Base path
* @param {string} target Sub path
*/
makeSafePath(base, target = null, force_forward_slash = false) {
if (target) {
target.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, "");
var targetPath = this.path.posix.normalize(target)
var output = this.fixPathSlashes(base + this.path.sep + targetPath);
} else {
base.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, "");
var targetPath = this.path.posix.normalize(base)
var output = this.fixPathSlashes(targetPath);
}
return (force_forward_slash) ? output.replace(this.path.sep, '/') : output;
}
makeSafeUsername(username) {
return username.replace(/^([A-Za-z0-9\-\_]{5,16})$/, '');
}
/**
* Corrects any / or \ differences, if any for file paths
* @param {string} path
* @returns {string} corrected path
*/
fixPathSlashes(path) {
// fix slashes
if (this.path.sep == '/' && path.indexOf("\\") != -1) path = path.replace(/\\/g, this.path.sep);
else if (this.path.sep == "\\" && path.indexOf("/") != -1) path = path.replace(/\//g, this.path.sep);
// remove double slashes
while (path.indexOf(this.path.sep + this.path.sep) != -1) path = path.replace(this.path.sep + this.path.sep, this.path.sep);
return path;
}
/**
* 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;
}
unpackCompressedB64(data) {
var data_buf = (typeof data === 'object') ? Buffer.from(data.toString('ascii'), 'base64') : Buffer.from(data, 'base64');
return this.zlib.inflateSync(data_buf, { finishFlush: this.zlib.Z_SYNC_FLUSH }).toString('ascii');
}
packCompressedB64(data) {
return this.zlib.deflateSync(data, { 'level': 9 }).toString('base64');
}
}
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;

View File

@@ -0,0 +1,219 @@
{
"config": {
"service_ip": "127.0.0.1",
"bind_ip": "0.0.0.0",
"ServiceVaults": [
"UserServiceVault",
"ServiceVault"
],
"ServiceDeps": "ServiceDeps",
"SessionStore": "SessionStore",
"SharedROMCache": "SharedROMCache",
"enable_shared_romcache": true,
"service_owner": "a minisrv user",
"service_owner_account": "minisrvuser",
"service_owner_contact": "someone",
"service_owner_contact_method": "something",
"service_name": "WebTV",
"service_logo": "WebTVLogoJewel.gif",
"service_splash_logo": "file://ROM/images/SplashLogo1.gif",
"hide_ssid_in_logs": true,
"filter_passwords_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",
"hide_incomplete_features": true,
"enable_lzpf_compression": true,
"enable_gzip_compression": true,
"show_detailed_splash": true,
"show_diskmap": false,
"unauthorized_url": "wtv-1800:/unauthorized?",
"allow_guests": true,
"domain_name": "wtv.zefie.com",
"user_accounts": {
"max_users_per_account": 6,
"min_username_length": 5,
"max_username_length": 18
},
"passwords": {
"enabled": true,
"min_length": 5,
"max_length": 32,
"form_size": 16
}
},
"services": {
"wtv-head-waiter": {
"port": 1601,
"connections": 1
},
"wtv-tricks": {
"port": 1602,
"flags": "0x00000004"
},
"wtv-star": {
"port": 1603,
"flags": "0x00000007"
},
"wtv-news": {
"port": 1605,
"disabled": true
},
"wtv-register": {
"port": 1607,
"privileged": true
},
"wtv-log": {
"port": 1609,
"connections": 1,
"write_logs_to_disk": false
},
"wtv-home": {
"port": 1612
},
"wtv-1800": {
"port": 1615,
"connections": 1,
"flags": "0x00000004",
"send_tellyscripts": false
},
"wtv-flashrom": {
"port": 1618,
"flags": "0x00000040",
"debug": false,
"use_zefie_server": true,
"bf0app_default_rom": "content/artemis-webtv-000/build7377/internal-nondebug/bf0app-part000.rom",
"catchall_file_name": "content-serve.js"
},
"wtv-setup": {
"port": 1613,
"flags": "0x00000010",
"passport_service": "wtv-passport",
"connections": 3,
"privileged": true
},
"wtv-music": {
"port": 1656,
"connections": 3
},
"wtv-cookie": {
"port": 1619,
"connections": 1
},
"wtv-chat": {
"port": 1630,
"connections": 3
},
"wtv-disk": {
"port": 1635,
"connections": 3
},
"wtv-guide": {
"port": 1621,
"connections": 3
},
"wtv-mail": {
"port": 1608,
"connections": 3
},
"wtv-passport": {
"port": 1654
},
"wtv-favorite": {
"port": 1611,
"connections": 3,
"enable_multi_query": true,
"max_folders": 14,
"max_favorites_per_folder": 18
},
"wtv-admin": {
"port": 1698,
"enable_multi_query": true,
"privileged": true,
"password": "viRak-7"
},
"http": {
"port": 1650,
"connections": 3,
"use_external_proxy": false,
"external_proxy_is_socks": false,
"external_proxy_host": "127.0.0.1",
"external_proxy_port": 1080,
"flags": "0x00000001"
},
"https": {
"port": 1650,
"connections": 3,
"use_external_proxy": false,
"external_proxy_is_socks": false,
"external_proxy_host": "127.0.0.1",
"external_proxy_port": 1080,
"flags": "0x00000001"
},
"pc_services": {
"port": 1699,
"pc_services": true,
"hide_minisrv_version": true,
"disabled": true,
"servicevault_dir": "http_pc",
"drop_connection_on_wrong_port": false,
"show_verbose_errors": false,
"privileged": true,
"allow_https": false,
"force_https": false
}
},
"favorites": {
"folder_templates": {
"Recommended": [
{
"title": "DuckDuckGo",
"url": "http://lite.duckduckgo.com/lite",
"image_type": "image/wtv-bitmap",
"image": "AAAAAAAAAQQAJAAAAAAAAAAAAAAAAAA0AAAARgAAAAABlAAAAAQBkLmAuYC5gLmAt3y5gJ1xsJKUgJeApoCIgLmAuYC5f7mAlG+umaVymJy5gLmAkoCMf5eAl4CigKOAuYC5gLN6sYe5gLmAm4CbgHCAlICdgJ6Ag22jqJ9znZ3YgNiAwIDAgJ54sZKlepmbuYC5gJuCm4CXgJeAo4CjgLmAuYCwebOHtH65gpd0qpmqcaKUmHCwlsWAj4C1gJCAjICMgLmAuYC5gLmAoXa3jZ+Aa4CggJOAl4CVgJuAo4CdgLmAfYC5gHKAbYCXgJaAuYCsgJeAj4CVcpalmW2jnH6AuoB5gLqAj4C7f5WAs3+ib5qcmG6omrmAuYChd7GQZ4B0gJeAloChcKqUl3Gjnbl5uYC5fbmAp3KrkJdxp5uUdZampXGdmbh9uYCWb6uZtX+xhZ54np2XbrmSn3CanotunKigcp2c2IXYgMCCwICqcZqYmnCmmrmAroC5gLmAlHOWpqBwopm0frGFl3Kin7d9uYCTbaubuYC5gKN3rpG4f66FmXOinYqAjIC5gLmAAAAAAAAAAAAAAAAAAAAAAAAAACUtBy4QLCQPHhAvARQDAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAwrKSAnJiMdIgoEGhEhAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAFDQgICAgICAgICAgICAgICAgICAgICBkCBg4WFwAAAIAAAAAcKAsLCwsLCwsLCwsLCwsLCwsLCwsLCxIVGB8JGwAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACowExMTAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
},
{
"title": "Old'aVista",
"url": "http://www.oldavista.com/",
"image_type": "url",
"image": "canned/oldavista.gif"
},
{
"title": "RetroSite",
"url": "http://retrosite.ga/",
"image_type": "url",
"image": "canned/retrosite.gif"
},
{
"title": "68k.news",
"url": "http://68k.news/",
"image_type": "url",
"image": "canned/news.gif"
},
{
"title": "The Old Net",
"url": "http://theoldnet.com/lite",
"image_type": "image/wtv-bitmap",
"image": "AAAAAAAAAQQAJAAAAAAAAAAAAAAAAAA0AAAARgAAAAACBAAAAAQCALmAuYC5gLmAuHy5ga9ftIu2f7iCmHWjoISusnuoj699knKVbK18rnm8gL2AuIC4gISdh3mojKN9uIC4gJudmXyRdJessXywiZdwjq+XdaShkauQebCLsH65gLmAsIG0gLWEt393q555lIKaf7uBlX94gHOAiICGgKtsto+yebiF2IDYgLqAuoCZkJyLp4iqhZmAmYDYgNiAp4CmgHKAfICjgKKAg4CUgLmElYC2gqSAt4CnfoiSnnqPgI+A0IDQgIyAkoCJgIeAk4CTgKGAzICfgHOAe4CHgIqAnX+RgMp/uYC5gJeAmYCOgJGAn4CjgL6AkYC4gJOAdIB4gKCAnICggJ2Ak4CdgI6AhoCggJKAqIS7gLKBtoCTfbt+s3+4gKOAo4CwgK+AjoCMgLeAuICvf6aAtoCygI+YmXyli6p9oICNgJ2AgoCHgI6AjICcgIWAlYC9gLyAuYC5gLKApn+SlIV6rYSrfnWuiXeejq58koOKf6qBo4C3f7mBnnW5kbiAuYCcdaFyzYDNgLOAs4C5gLmAo4CngLmAt4CvgJF/noB1f7aAtYCAgG+AtIC3gG2AiYC1gLKAs4CNgLaAtoCWgJOAeIBtgJGAuYCSgLmAnJSNe6KOoX2kgHiAqoCMgKSApoCBgHqAuYC4gLiAsoCXgJuAcoB3gKaAooCYgHiAAAAAADMMBwcHBwcHAi8BAQEWDAwwMDAwMDACAgICLwAAAAAAAAAAADQKLQMGCgYDCA8PDw8NBgMEBAQEBCMICQkIDwAAAACIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAA9CwsLCwsLCwsLCws9HBwcHBwzHBwLAAAAAAAAAAAAAAAeOBQpDS4dISQuGxISEhINKDg+KTsgOBQ5AAAAAAAHAAAAAAAAJgAmBQUFBQUFAAUFBRwgPj4+Pho/PiAjAAAAAAAGAAAAAAAAHiEZEhISEhISEhISDT88Ezw/ExMTIDkAAAAAAABVAAAAAAAANwsFMTExMTExMTExFRQYGBgYGBgYPCMAAAAAAAB5AAAAAAAAHi4lFxcXFxcXFxcXDSgYGCk+GBgYKTkAAAAAAABVAAAAAAAAFSIiEBAQEBAQEBAQHhoODhgODg4OGDkAAAAAAABVAAAAAAAAMxwcHBwcHBwcHBwcHCEdHR0pHSEdHTIAAAAAAABVAAAAAAAAHiojEBAQEBAQEBAQHjsfHx8fDiEfKTkAAAAAAAB6AAAAAAEAAAAAPQAAAAAAAAAAACUlJSUlJSUlJQsLCwsAAABVAAAAPSYkJiYkJiQmJCQkJCMkJiQkJCIkJCYkJCQkJCIAAABVAAAAAAAAAAAAJiYmJCYkJiQkJCQmJCYREQsAAAAAAAAAAABVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB6AAAAAAAAAAAAAAAAKws9Cz0LACsAPQsAAAAAAAAAAAAAAABVAAAAAAAAAAAAAAAAJAYnLCcnBgYnBiIAAAAAAAAAAAAAAABVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABVAAAzMjMcMzIyMjIrHDIyMisyMjILHDIyCzMzMjIyMisLAAB6AAA3NTc2JTY0JTY0NTU1JSU2NTU3NTU1NDUlNjU1NjQqAABVPQAAAD0AAAAAAD0ANzYfDh0fHyEfHwsAPQsAAAAAAAA9AABVFR0pIS4pLjonJzonJzoVJzo6OjoGOicnLjY7ITsoIRg7ITlVAAAAAAAAAAAAAAAAAAAoGBQ/IBQUAAAAAAAAAAAAAAAAAAB6ADMTPD8TPhM/ExM8OD88PzgUExQ8Ez4UPBQTExQ+PD8UOQBV"
}
],
"Personal": [
{
"title": "The Midnight Archive",
"url": "http://archive.midnightchannel.net/",
"image_type": "url",
"image": "canned/midnightchannel.gif"
},
{
"title": "WebTV/MSNTV Secret Pics & Info",
"url": "http://turdinc.kicks-ass.net/Msntv/index2.html",
"image_type": "url",
"image": "canned/mattman69.gif"
}
]
}
}
}