clean up file structure
This commit is contained in:
176
zefie_wtvp_minisrv/includes/classes/WTVAdmin.js
Normal file
176
zefie_wtvp_minisrv/includes/classes/WTVAdmin.js
Normal file
@@ -0,0 +1,176 @@
|
||||
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(justchecking = false) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (justchecking) {
|
||||
return (allowed_ssid && allowed_ip) ? true : false;
|
||||
} else {
|
||||
return (allowed_ssid && allowed_ip) ? true : this.rejectConnection(!allowed_ssid);
|
||||
}
|
||||
}
|
||||
|
||||
getAccountInfo(username, directory = null) {
|
||||
var search_dir = this.minisrv_config.config.SessionStore + this.path.sep + "accounts";
|
||||
var account_data = null;
|
||||
var self = this;
|
||||
if (directory) search_dir = directory;
|
||||
this.fs.readdirSync(search_dir).forEach(file => {
|
||||
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && account_data === null) {
|
||||
account_data = self.getAccountInfo(username, search_dir + self.path.sep + file);
|
||||
}
|
||||
if (account_data !== null) return;
|
||||
if (!file.match(/.*\.json/ig)) return;
|
||||
try {
|
||||
var temp_session_data_file = self.fs.readFileSync(search_dir + self.path.sep + file, 'Utf8');
|
||||
var temp_session_data = JSON.parse(temp_session_data_file);
|
||||
|
||||
if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) {
|
||||
account_data = [temp_session_data, (search_dir + self.path.sep + file).replace(this.minisrv_config.config.SessionStore + this.path.sep + "accounts", "").split(this.path.sep)[1]];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(" # Error parsing Session Data JSON", search_dir + self.path.sep + file, e);
|
||||
}
|
||||
});
|
||||
if (account_data !== null) {
|
||||
if (account_data.ssid) return account_data;
|
||||
var account_info = {};
|
||||
account_info.ssid = account_data[1];
|
||||
account_info.username = account_data[0].subscriber_username;
|
||||
account_info.user_id = account_data[0].subscriber_userid;
|
||||
var userSession = new this.WTVClientSessionData(this.minisrv_config, account_info.ssid);
|
||||
userSession.user_id = 0;
|
||||
account_info.account_users = userSession.listPrimaryAccountUsers();
|
||||
return account_info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getAccountInfoBySSID(ssid) {
|
||||
var account_info = {};
|
||||
var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid);
|
||||
userSession.user_id = 0;
|
||||
if (userSession.isRegistered(false)) {
|
||||
account_info.ssid = ssid;
|
||||
account_info.account_users = userSession.listPrimaryAccountUsers();
|
||||
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;
|
||||
1412
zefie_wtvp_minisrv/includes/classes/WTVBGMusic.js
Normal file
1412
zefie_wtvp_minisrv/includes/classes/WTVBGMusic.js
Normal file
File diff suppressed because it is too large
Load Diff
156
zefie_wtvp_minisrv/includes/classes/WTVClientCapabilities.js
Normal file
156
zefie_wtvp_minisrv/includes/classes/WTVClientCapabilities.js
Normal 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;
|
||||
864
zefie_wtvp_minisrv/includes/classes/WTVClientSessionData.js
Normal file
864
zefie_wtvp_minisrv/includes/classes/WTVClientSessionData.js
Normal file
@@ -0,0 +1,864 @@
|
||||
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 = minisrv_config.config.lockdownWhitelist;
|
||||
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");
|
||||
}
|
||||
|
||||
assignMailStore() {
|
||||
this.mailstore = new WTVMail(this.minisrv_config, this)
|
||||
}
|
||||
|
||||
assignFavoriteStore() {
|
||||
this.mailstore = this.favstore = new WTVFavorites(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;
|
||||
var accounts = this.listPrimaryAccountUsers();
|
||||
var self = this;
|
||||
Object.keys(accounts).forEach((k) => {
|
||||
var user_id = accounts[k].user_id;
|
||||
var subUserSession = new self.constructor(self.minisrv_config, self.ssid);
|
||||
subUserSession.switchUserID(user_id, false, false);
|
||||
subUserSession.assignMailStore();
|
||||
if (subUserSession.mailstore) {
|
||||
total_unread_messages += subUserSession.mailstore.countUnreadMessages(0);
|
||||
}
|
||||
});
|
||||
return total_unread_messages;
|
||||
}
|
||||
|
||||
clearUserSessionMemory() {
|
||||
this.setUserLoggedIn(false);
|
||||
this.data_store = new Array();
|
||||
this.session_store = {};
|
||||
this.assignFavoriteStore();
|
||||
this.assignMailStore()
|
||||
}
|
||||
|
||||
switchUserID(user_id, update_mail = true, update_ticket = true, update_favorite = true) {
|
||||
this.user_id = parseInt(user_id);
|
||||
if (user_id != null) {
|
||||
this.loadSessionData();
|
||||
if (this.isRegistered() && update_mail) this.assignMailStore();
|
||||
if (this.isRegistered() && update_favorite) this.assignMailStore();
|
||||
if (this.data_store.wtvsec_login && update_ticket) this.setTicketData('user_id', user_id);
|
||||
} else {
|
||||
this.user_id = 0;
|
||||
this.clearUserSessionMemory();
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
account_data['subscriber'].user_id = 0;
|
||||
}
|
||||
else {
|
||||
account_data[f] = JSON.parse(this.fs.readFileSync(user_file));
|
||||
account_data[f].user_id = parseInt(f.replace("user", ''))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
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;
|
||||
},
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
getAccountStoreDirectory() {
|
||||
return this.minisrv_config.config.SessionStore + this.path.sep + "accounts";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getAccountStoreDirectory() + 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);
|
||||
}
|
||||
|
||||
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() {
|
||||
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 sessionToStore = this.session_store;
|
||||
var json_save_data = JSON.stringify(sessionToStore);
|
||||
var json_load_data = (skip_merge) ? {} : this.loadSessionData(true);
|
||||
|
||||
var storeDir = this.getUserStoreDirectory();
|
||||
if (!this.fs.existsSync(storeDir)) this.mkdirRecursive(storeDir);
|
||||
|
||||
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.getAccountStoreDirectory()), 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;
|
||||
357
zefie_wtvp_minisrv/includes/classes/WTVDisk.js
Normal file
357
zefie_wtvp_minisrv/includes/classes/WTVDisk.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 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=${encodeURIComponent(group)}&source=${encodeURIComponent(url)}&message=${encodeURIComponent(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>
|
||||
|
||||
<br>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WTVDownloadList;
|
||||
403
zefie_wtvp_minisrv/includes/classes/WTVFavorites.js
Normal file
403
zefie_wtvp_minisrv/includes/classes/WTVFavorites.js
Normal file
@@ -0,0 +1,403 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
checkFolderName(foldername) {
|
||||
var regex_str = "^([A-Za-z0-9\-\_]{1,}$";
|
||||
var regex = new RegExp(regex_str);
|
||||
return regex.test(foldername);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
195
zefie_wtvp_minisrv/includes/classes/WTVFlashrom.js
Normal file
195
zefie_wtvp_minisrv/includes/classes/WTVFlashrom.js
Normal file
@@ -0,0 +1,195 @@
|
||||
class WTVFlashrom {
|
||||
|
||||
fs = require('fs');
|
||||
https = require('follow-redirects').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)
|
||||
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) {
|
||||
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;
|
||||
182
zefie_wtvp_minisrv/includes/classes/WTVGuide.js
Normal file
182
zefie_wtvp_minisrv/includes/classes/WTVGuide.js
Normal file
@@ -0,0 +1,182 @@
|
||||
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");
|
||||
const 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.getTemplate("wtv-guide", "templates/glossary.js", true);
|
||||
var glossary_datafile =this.wtvshared.getTemplate("wtv-guide", "glossary.json", true);
|
||||
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">
|
||||
if (definition.substr(start, 1) != ">") {
|
||||
start++; // +1 to skip =
|
||||
end = 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 {
|
||||
start++;
|
||||
}
|
||||
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);
|
||||
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.getTemplate("wtv-guide", "templates/glossary_word_index.js", true);
|
||||
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.getTemplate("wtv-guide", "templates/glossary_index.js", true);
|
||||
var glossary_datafile =this.wtvshared.getTemplate("wtv-guide", "glossary.json", true);
|
||||
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.getTemplate("wtv-guide", "prerendered/" + topic + "/" + subtopic + ".js", true);
|
||||
else prerendered =this.wtvshared.getTemplate("wtv-guide", "prerendered/" + topic + "/" + subtopic + "/" + page + ".js", true);
|
||||
|
||||
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
|
||||
this.wtvshared.unloadModule(template);
|
||||
}
|
||||
|
||||
// return generated page
|
||||
return data;
|
||||
} else return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WTVGuide;
|
||||
522
zefie_wtvp_minisrv/includes/classes/WTVLzpf.js
Normal file
522
zefie_wtvp_minisrv/includes/classes/WTVLzpf.js
Normal 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;
|
||||
550
zefie_wtvp_minisrv/includes/classes/WTVMail.js
Normal file
550
zefie_wtvp_minisrv/includes/classes/WTVMail.js
Normal file
@@ -0,0 +1,550 @@
|
||||
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: "#191919",
|
||||
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, allow_html = false) {
|
||||
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,
|
||||
"allow_html": allow_html
|
||||
}
|
||||
try {
|
||||
if (this.fs.existsSync(message_file_out)) {
|
||||
console.error(" * 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 welcomeTemplate = this.wtvshared.getTemplate("wtv-mail", "welcomeMail.txt").toString('ascii');
|
||||
var end_of_headers = false;
|
||||
var msg = "";
|
||||
var self = this;
|
||||
var to_addr = this.wtvclient.getSessionData("subscriber_username") + "@" + this.minisrv_config.config.service_name;
|
||||
var to_name = this.wtvclient.getSessionData("subscriber_name");
|
||||
var available_tags = {
|
||||
...this.minisrv_config.config,
|
||||
"user_address": to_addr,
|
||||
"user_name": to_name
|
||||
}
|
||||
var from_name, from_addr, subj = null;
|
||||
var lines = welcomeTemplate.replace(/\r/g, '').split("\n");
|
||||
lines.forEach((line) => {
|
||||
if (line.indexOf(": ") > 1 && !end_of_headers) {
|
||||
var header = [line.slice(0, line.indexOf(':')), line.slice(line.indexOf(':') + 2).trim()];
|
||||
switch (header[0].toLowerCase()) {
|
||||
case "from":
|
||||
if (header[1].indexOf("<") >= 0) {
|
||||
var email = header[1].match(/(.+) \<(.+)\>/);
|
||||
if (email) {
|
||||
from_name = email[1];
|
||||
from_addr = email[2];
|
||||
} else {
|
||||
var email = header[1].match(/\<(.+)\>/);
|
||||
from_addr = email[1];
|
||||
}
|
||||
} else if (header[1].indexOf('@') >= 0) {
|
||||
from_addr = header[1];
|
||||
}
|
||||
break;
|
||||
|
||||
case "subject":
|
||||
subj = header[1];
|
||||
break;
|
||||
}
|
||||
} else if (line == '') end_of_headers = true;
|
||||
else {
|
||||
msg += line.replace(/\$\{(\w{1,})\}/g, function (x) {
|
||||
var out = '';
|
||||
var tag = x.replace("${", '').replace('}', '');
|
||||
if (available_tags[tag]) out = available_tags[tag];
|
||||
return out
|
||||
}) + "\n";
|
||||
}
|
||||
});
|
||||
return this.createMessage(0, from_addr, to_addr, msg, subj, from_name, to_name, null, null, true, [], 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 + "accounts" + 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(0);
|
||||
// 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;
|
||||
266
zefie_wtvp_minisrv/includes/classes/WTVMime.js
Normal file
266
zefie_wtvp_minisrv/includes/classes/WTVMime.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Simple class for WebTV Mime Types and overrides
|
||||
*/
|
||||
|
||||
|
||||
class WTVMime {
|
||||
|
||||
mime = require('mime-types');
|
||||
wtvshared = null;
|
||||
minisrv_config = [];
|
||||
|
||||
|
||||
constructor(minisrv_config) {
|
||||
const { WTVShared } = require("./WTVShared.js");
|
||||
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);
|
||||
}
|
||||
|
||||
// modified from https://github.com/sergi/mime-multipart/blob/master/index.js
|
||||
|
||||
generateMultipartMIME(tuples, options) {
|
||||
// modified for creating usenet compliant headers/content from an attachment
|
||||
var CRLF = '\n';
|
||||
if (tuples.length === 0) {
|
||||
// according to rfc1341 there should be at least one encapsulation
|
||||
throw new Error('Missing argument. At least one part to generate is required');
|
||||
}
|
||||
|
||||
options = options || {};
|
||||
var preamble = options.preamble || "This is a multi-part message in MIME format.";
|
||||
var epilogue = options.epilogue;
|
||||
var boundary = options.boundary || "------------" + this.wtvshared.generateString(24);
|
||||
|
||||
if (boundary.length < 1 || boundary.length > 70) {
|
||||
throw new Error('Boundary should be between 1 and 70 characters long');
|
||||
}
|
||||
|
||||
var boundary_header = 'multipart/mixed; boundary="' + boundary + '"';
|
||||
|
||||
var delimiter = CRLF + '--' + boundary;
|
||||
var closeDelimiter = delimiter + '--';
|
||||
|
||||
var wtvshared = this.wtvshared;
|
||||
|
||||
var encapsulations = tuples.map(function (tuple, i) {
|
||||
var mimetype = tuple.mime || 'text/plain';
|
||||
var encoding = tuple.encoding || 'utf-8';
|
||||
var use_base64 = tuple.use_base64 || !wtvshared.isASCII(tuple.content);
|
||||
var is_base64 = tuple.is_base64 || wtvshared.isBase64(tuple.content);
|
||||
var filename = (tuple.filename) ? tuple.filename : (use_base64) ? ('file' + i) : null;
|
||||
|
||||
var headers = [
|
||||
`Content-Type: ${mimetype}; ${(use_base64) ? `name="${filename}"` : `charset=${encoding.toUpperCase()}; format=flowed`}`,
|
||||
];
|
||||
|
||||
if (filename) headers.push(`Content-Disposition: attachment; filename="${filename}"`);
|
||||
headers.push(`Content-Transfer-Encoding: ${(use_base64) ? 'base64' : '7bit'}`);
|
||||
|
||||
var bodyPart = headers.join(CRLF) + CRLF + CRLF;
|
||||
if (use_base64 && !is_base64) bodyPart += wtvshared.lineWrap(Buffer.from(tuple.content).toString('base64'),72) + CRLF;
|
||||
else bodyPart += wtvshared.lineWrap(tuple.content,72);
|
||||
|
||||
return delimiter + CRLF + bodyPart;
|
||||
});
|
||||
|
||||
var multipartBody = [
|
||||
preamble ? preamble : undefined,
|
||||
encapsulations.join(''),
|
||||
closeDelimiter,
|
||||
epilogue ? CRLF + epilogue : undefined,
|
||||
].filter(function (element) { return !!element; });
|
||||
|
||||
return {
|
||||
"mime_version": "1.0",
|
||||
"content_type": boundary_header,
|
||||
"content": multipartBody.join('')
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WTVMime;
|
||||
532
zefie_wtvp_minisrv/includes/classes/WTVNews.js
Normal file
532
zefie_wtvp_minisrv/includes/classes/WTVNews.js
Normal file
@@ -0,0 +1,532 @@
|
||||
class WTVNews {
|
||||
|
||||
minisrv_config = null;
|
||||
newsie = require('newsie').default;
|
||||
strftime = require('strftime');
|
||||
wtvshared = null;
|
||||
service_name = null;
|
||||
client = null;
|
||||
username = null;
|
||||
password = null;
|
||||
posting_allowed = true;
|
||||
debug = null;
|
||||
|
||||
constructor(minisrv_config, service_name) {
|
||||
this.minisrv_config = minisrv_config;
|
||||
this.service_name = service_name;
|
||||
const { WTVShared } = require("./WTVShared.js");
|
||||
this.wtvshared = new WTVShared(minisrv_config);
|
||||
this.debug = require('debug')('WTVNews');
|
||||
}
|
||||
|
||||
initializeUsenet(host, port = 119, tls_options = null, username = null, password = null) {
|
||||
// use local self-signed cert for local server
|
||||
var newsie_options = {
|
||||
host: host,
|
||||
port: port,
|
||||
tlsPort: (tls_options !== null) ? true : false,
|
||||
}
|
||||
if (newsie_options.tlsPort) newsie_options.tlsOptions = tls_options;
|
||||
this.client = new this.newsie(newsie_options);
|
||||
if (username && password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
connectUsenet() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.connect().then((response) => {
|
||||
if (response.code == 200 || response.code == 201) {
|
||||
if (response.code == 201) this.posting_allowed = false;
|
||||
if (this.username && this.password) {
|
||||
this.client.authInfoUser(this.username).then((res) => {
|
||||
if (res.code == "381") {
|
||||
res.authInfoPass(this.password).then((res) => {
|
||||
if (res.code == 281) resolve(true);
|
||||
else reject(res.description);
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: connect", e);
|
||||
reject("Could not connect to upstream usenet server");
|
||||
});
|
||||
} else {
|
||||
reject(res.description)
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: connect", e);
|
||||
reject("Could not connect to upstream usenet server");
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: connect", e);
|
||||
reject("Could not connect to upstream usenet server");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
listGroup(group, page = 0, limit = 100, raw_range = null) {
|
||||
// list of articles from group
|
||||
return new Promise((resolve, reject) => {
|
||||
this.selectGroup(group).then((res) => {
|
||||
if (!raw_range) {
|
||||
var range = {
|
||||
start: (limit * page) + res.group.low,
|
||||
}
|
||||
range.end = range.start + limit;
|
||||
if (page) range.start++;
|
||||
if (range.end > res.high) delete range.group.end;
|
||||
} else {
|
||||
range = raw_range;
|
||||
}
|
||||
this.client.listGroup(group, range).then((data) => {
|
||||
resolve(data);
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: listGroup", e);
|
||||
reject(`No such group <b>${group}</b>`);
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: selectGroup", e);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
processGroupList(list) {
|
||||
if (list) return list.newsgroups;
|
||||
else return null;
|
||||
}
|
||||
|
||||
listGroups(search = null) {
|
||||
// list of groups on the server
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!search) {
|
||||
this.client.list().then((data) => {
|
||||
this.debug('listGroups data', data)
|
||||
resolve(this.processGroupList(data));
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: listGroups (all)", e);
|
||||
reject(e);
|
||||
});
|
||||
} else {
|
||||
this.client.listNewsgroups((search === '*') ? '*' : '*' + search + '*').then((data) => {
|
||||
resolve(this.processGroupList(data));
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: listGroups (search)", search, e);
|
||||
reject(e);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
selectGroup(group) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.group(group).then((response) => {
|
||||
if (response.code == 211) resolve(response);
|
||||
else reject(`No such group <b>${group}</b>`);
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: selectGroup", e);
|
||||
reject(`Error selecting group <b>${group}</b>`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getArticle(articleID, get_next_last = true) {
|
||||
var articleID = parseInt(articleID);
|
||||
return new Promise((resolve, reject) => {
|
||||
var promises = [];
|
||||
this.client.article(articleID).then((data) => {
|
||||
if (get_next_last) {
|
||||
// ask server for next article
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
this.client.next().then((res) => {
|
||||
data.next_article = res.article.articleNumber;
|
||||
resolve(data.next_article);
|
||||
}).catch((e) => {
|
||||
data.next_article = null;
|
||||
resolve(data.next_article);
|
||||
})
|
||||
}));
|
||||
|
||||
// ask server for previous article
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
this.client.last().then((res) => {
|
||||
data.prev_article = res.article.articleNumber;
|
||||
if (data.prev_article === articleID) {
|
||||
// do it again, needed this for CodoSoft NNTPd?
|
||||
this.client.article(data.prev_article).then(() => {
|
||||
this.client.last().then((res) => {
|
||||
data.prev_article = res.article.articleNumber;
|
||||
resolve(data.prev_article);
|
||||
}).catch(() => {
|
||||
data.prev_article = null;
|
||||
resolve(data.prev_article);
|
||||
});
|
||||
}).catch(() => {
|
||||
data.prev_article = null;
|
||||
resolve(data.prev_article);
|
||||
});
|
||||
} else {
|
||||
resolve(data.prev_article);
|
||||
}
|
||||
}).catch(() => {
|
||||
data.prev_article = null;
|
||||
resolve(data.prev_article);
|
||||
})
|
||||
}));
|
||||
|
||||
Promise.all(promises).then(() => {
|
||||
var self = this;
|
||||
if (data.article.headers) Object.keys(data.article.headers).forEach((k) => {
|
||||
data.article.headers[k] = self.decodeCharset(data.article.headers[k])
|
||||
});
|
||||
resolve(data);
|
||||
});
|
||||
} else {
|
||||
var self = this;
|
||||
if (data.article.headers) Object.keys(data.article.headers).forEach((k) => {
|
||||
data.article.headers[k] = self.decodeCharset(data.article.headers[k])
|
||||
});
|
||||
resolve(data);
|
||||
}
|
||||
}).catch((e) => {
|
||||
reject(`Error reading article ID ${articleID}`);
|
||||
console.error(" * WTVNews Error:", "Command: article", "args:", articleID, "Error:", e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
decodeCharset(string) {
|
||||
var regex = /=\?{1}(.+)\?{1}([B|Q])\?{1}(.+)\?{1}=/;
|
||||
var decoded = null;
|
||||
var check = string.match(regex);
|
||||
if (check) {
|
||||
var match = check[0];
|
||||
var charset = check[1];
|
||||
var encoding = check[2];
|
||||
var encoded_text = check[3];
|
||||
switch (encoding) {
|
||||
case "B":
|
||||
var buffer = new Buffer.from(encoded_text, 'base64')
|
||||
decoded = buffer.toString(charset).replace(/[^\x00-\x7F]/g, "");;
|
||||
break;
|
||||
|
||||
case "Q":
|
||||
// unimplemented
|
||||
return string;
|
||||
}
|
||||
if (decoded) return string.replace(match, decoded);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
getHeader(articleID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.head(articleID).then((data) => {
|
||||
var self = this;
|
||||
if (data.article.headers) Object.keys(data.article.headers).forEach((k) => {
|
||||
data.article.headers[k] = self.decodeCharset(data.article.headers[k])
|
||||
});
|
||||
resolve(data);
|
||||
}).catch((e) => {
|
||||
reject(`Error getting header for article ID ${articleID}`);
|
||||
console.error(" * WTVNews Error:", "Command: head -", "Article ID: " + articleID, e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getHeaderFromMessage(message, header) {
|
||||
var response = null;
|
||||
if (message.article.headers) {
|
||||
Object.keys(message.article.headers).forEach((k) => {
|
||||
if (k.toLowerCase() == header.toLowerCase()) {
|
||||
response = message.article.headers[k];
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
quitUsenet() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.quit().then((response) => {
|
||||
if (response.code == 205) resolve(true);
|
||||
else {
|
||||
console.error(" * WTVNews Error:", "Command: quit", e);
|
||||
reject(`Unexpected response code ${response.code}`);
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.error(" * WTVNews Error:", "Command: quit", e);
|
||||
reject("Error quitting usenet session");
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
postToGroup(group, from_addr, msg_subject, msg_body, article = null, headers = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var promises = [];
|
||||
var messageid = null;
|
||||
this.connectUsenet()
|
||||
.then(() => {
|
||||
if (article) {
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
this.selectGroup(group).then((res) => {
|
||||
this.getArticleMessageID(article).then((data) => {
|
||||
messageid = data;
|
||||
resolve(data);
|
||||
}).catch((e) => {
|
||||
console.error('Error getting articleID',article, e)
|
||||
reject(e)
|
||||
});
|
||||
}).catch((e) => {
|
||||
console.error('Error selecting group', e)
|
||||
reject(e)
|
||||
});
|
||||
}));
|
||||
}
|
||||
Promise.all(promises).then(() => {
|
||||
this.client.post()
|
||||
.then((response) => {
|
||||
if (response.code == 340) {
|
||||
var articleData = {};
|
||||
articleData.headers = {
|
||||
'Relay-Version': "version zefie_wtvp_minisrv " + this.minisrv_config.version + "; site " + this.minisrv_config.config.domain_name,
|
||||
'Posting-Version': "version zefie_wtvp_minisrv " + this.minisrv_config.version + "; site " + this.minisrv_config.config.domain_name,
|
||||
'Path': "@" + this.minisrv_config.config.domain_name,
|
||||
'From': from_addr,
|
||||
'Newsgroups': group,
|
||||
'Subject': msg_subject || "(No subject)",
|
||||
'Message-ID': "<" + this.wtvshared.generateString(16) + "@" + this.minisrv_config.config.domain_name + ">",
|
||||
'Date': this.strftime('%a, %-d %b %Y %H:%M:%S %z', new Date())
|
||||
}
|
||||
if (headers) {
|
||||
Object.keys(headers).forEach((k) => {
|
||||
articleData.headers[k] = headers[k];
|
||||
});
|
||||
}
|
||||
if (messageid) {
|
||||
articleData.headers.References = messageid;
|
||||
articleData.headers['In-Reply-To'] = messageid;
|
||||
}
|
||||
if (msg_body) articleData.body = msg_body.split("\n");
|
||||
else articleData.body = [];
|
||||
|
||||
response.send(articleData).then((response) => {
|
||||
this.client.quit();
|
||||
if (response.code !== 240) {
|
||||
reject("Could not send post. Server returned error " + response.code);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
this.client.quit();
|
||||
reject("Could not send post. Server returned error " + response.code);
|
||||
});
|
||||
} else {
|
||||
this.client.quit();
|
||||
console.error('usenet upstream uncaught error', e);
|
||||
reject("Could not send post. Server returned unknown error");
|
||||
};
|
||||
}).catch((e) => {
|
||||
console.error('could not connect to server', e);
|
||||
reject("could not connect to server");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getArticleMessageID(articleID) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.article(articleID).then((data) => {
|
||||
resolve(data.article.messageId);
|
||||
}).catch((e) => {
|
||||
console.error("error getting messageID from article", articleID, e)
|
||||
reject(e);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getHeaderObj(NGArticles) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var messages = [];
|
||||
var promises = [];
|
||||
for (var article in NGArticles) {
|
||||
if (article == "getCaseInsensitiveKey") continue;
|
||||
promises.push(new Promise((resolve, reject) => {
|
||||
this.getHeader(NGArticles[article]).then((data) => {
|
||||
if (data.article) messages.push(data.article)
|
||||
resolve();
|
||||
}).catch((e) => {
|
||||
reject(e);
|
||||
});
|
||||
}));
|
||||
}
|
||||
if (promises.length > 0) {
|
||||
Promise.all(promises).then(() => {
|
||||
if (messages.length > 0) resolve(messages);
|
||||
}).catch((e) => {
|
||||
reject("Could not read message list", e);
|
||||
});
|
||||
} else {
|
||||
resolve(messages);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
parseAttachments(message) {
|
||||
var contype = this.getHeaderFromMessage(message, 'Content-Type');
|
||||
if (contype) {
|
||||
var regex = /multipart\/mixed\; boundary=\"(.+)\"/i;
|
||||
var match = contype.match(regex);
|
||||
if (match) {
|
||||
var boundary = "--" + match[1];
|
||||
var body = message.article.body.join("\n").split(boundary);
|
||||
var attachments = [];
|
||||
var i = 0;
|
||||
var message_body = '';
|
||||
var message_type = 'text/plain';
|
||||
body.forEach((element) => {
|
||||
var section_type = null;
|
||||
var section = element.split("\n");
|
||||
attachments[i] = {};
|
||||
section.forEach((line) => {
|
||||
this.debug('section_type', section_type, 'line', line);
|
||||
var section_header_match = line.match(/^Content\-/i)
|
||||
if (section_header_match) {
|
||||
var section_match = line.match(/^Content\-Type\: (.+)\;/i)
|
||||
if (section_match) {
|
||||
this.debug('section_match', section_match)
|
||||
section_type = section_match[1];
|
||||
if (section_match[1].match("text/plain")) {
|
||||
message_type = section_type;
|
||||
} else {
|
||||
section_type = section_match[1];
|
||||
attachments[i].content_type = section_match[1]
|
||||
}
|
||||
this.debug('section_type', section_type)
|
||||
}
|
||||
section_match = line.match(/^Content\-Disposition\: (.+)\;/i)
|
||||
if (section_match) {
|
||||
section_match = line.match(/^Content\-Disposition\: (.+)\; filename=\"(.+)\"/i)
|
||||
if (section_match) attachments[i].filename = section_match[2];
|
||||
}
|
||||
section_match = line.match(/^Content-Transfer-Encoding: (.+)/i)
|
||||
if (section_match) attachments[i].content_encoding = section_match[1];
|
||||
} else {
|
||||
if (section_type != null) {
|
||||
if (section_type.match("text/plain")) message_body += line;
|
||||
else {
|
||||
if (attachments[i].data) attachments[i].data += line;
|
||||
else attachments[i].data = line;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (attachments[i].content_type) i++;
|
||||
})
|
||||
attachments.pop();
|
||||
return {
|
||||
text: message_body,
|
||||
text_type: message_type || "text/plain",
|
||||
attachments: attachments
|
||||
}
|
||||
} else {
|
||||
var message_body = '';
|
||||
if (message.article.body) message_body = message.article.body.join("\n")
|
||||
|
||||
return { text: message_body }
|
||||
}
|
||||
} else {
|
||||
var message_body = '';
|
||||
if (message.article.body) message_body = message.article.body.join("\n")
|
||||
|
||||
return { text: message_body }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sortByResponse(messages) {
|
||||
var sorted = [];
|
||||
var message_id_roots = [];
|
||||
var message_relations = [];
|
||||
Object.keys(messages).forEach((k) => {
|
||||
var messageId = messages[k].messageId;
|
||||
var ref = messages[k].headers.REFERENCES;
|
||||
if (ref) {
|
||||
var res = message_id_roots.find(e => e.messageId == ref);
|
||||
if (res) {
|
||||
// see if its attached to a root post
|
||||
if (message_relations[res.messageId]) message_relations[res.messageId].push({ "messageId": messageId, "index": k });
|
||||
else message_relations[res.messageId] = [{ "messageId": messageId, "index": k }];
|
||||
} else {
|
||||
// see if its related to a relation
|
||||
if (Object.keys(message_relations).length > 0) {
|
||||
var found = false;
|
||||
Object.keys(message_relations).forEach((j) => {
|
||||
if (message_relations[j].length > 0) {
|
||||
Object.keys(message_relations[j]).forEach((h) => {
|
||||
if (found) return;
|
||||
if (message_relations[j][h].messageId == ref) {
|
||||
var searchref = messages[message_relations[j][h].index].headers.REFERENCES || null;
|
||||
var mainref = null;
|
||||
while (searchref !== null) {
|
||||
var searchart = messages.find(e => e.messageId == searchref);
|
||||
var searchref = searchart.headers.REFERENCES || null;
|
||||
}
|
||||
mainref = searchart.messageId;
|
||||
message_relations[mainref].push({ "messageId": messageId, "index": k });
|
||||
found = true;
|
||||
} else {
|
||||
// no relation, missing reference, add as root
|
||||
message_id_roots.push({ "messageId": messageId, "index": k });
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
} else {
|
||||
message_id_roots.push({ "messageId": messageId, "index": k });
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
message_id_roots.push({ "messageId": messageId, "index": k });
|
||||
}
|
||||
});
|
||||
|
||||
// sort the relations, putting root articles first, followed by their relations
|
||||
var message_roots_sorted = [];
|
||||
Object.keys(message_id_roots).forEach((k) => {
|
||||
// sort relations by date
|
||||
var article = messages[message_id_roots[k].index];
|
||||
var article_date = Date.parse(article.headers.DATE);
|
||||
message_roots_sorted.push({ "article": article, "relation": null, "date": article_date });
|
||||
});
|
||||
message_roots_sorted.sort((a, b) => { return (a.date - b.date) });
|
||||
Object.keys(message_roots_sorted).forEach((k) => {
|
||||
sorted.push(message_roots_sorted[k]);
|
||||
|
||||
if (message_relations[message_id_roots[k].messageId]) {
|
||||
var relations = [];
|
||||
Object.keys(message_relations[message_id_roots[k].messageId]).forEach((j) => {
|
||||
// sort relations by date
|
||||
var article = messages[message_relations[message_id_roots[k].messageId][j].index];
|
||||
var article_date = Date.parse(article.headers.DATE);
|
||||
relations.push({ "article": article, "relation": message_id_roots[k].messageId || null, "date": article_date })
|
||||
});
|
||||
relations.sort((a, b) => { return (a.date - b.date) });
|
||||
Object.keys(relations).forEach((j) => {
|
||||
sorted.push(relations[j]);
|
||||
});
|
||||
}
|
||||
})
|
||||
return sorted;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = WTVNews;
|
||||
475
zefie_wtvp_minisrv/includes/classes/WTVNewsServer.js
Normal file
475
zefie_wtvp_minisrv/includes/classes/WTVNewsServer.js
Normal file
@@ -0,0 +1,475 @@
|
||||
class WTVNewsServer {
|
||||
fs = require('fs');
|
||||
path = require('path');
|
||||
minisrv_config = null;
|
||||
strftime = require('strftime');
|
||||
wtvshared = null;
|
||||
username = null;
|
||||
password = null;
|
||||
using_auth = false;
|
||||
local_server = null;
|
||||
data_path = null;
|
||||
featuredGroups = null
|
||||
|
||||
constructor(minisrv_config, local_server_port, using_auth = false, username = null, password = null, run_server = true) {
|
||||
this.minisrv_config = minisrv_config;
|
||||
const { WTVShared } = require("./WTVShared.js");
|
||||
this.wtvshared = new WTVShared(minisrv_config);
|
||||
this.featuredGroups = minisrv_config.services['wtv-news'].featuredGroups;
|
||||
const nntp_server = require('nntp-server-zefie');
|
||||
var nntp_statuses = require('nntp-server-zefie/lib/status');
|
||||
|
||||
this.username = username || null;
|
||||
this.password = password || null;
|
||||
this.using_auth = using_auth;
|
||||
this.scan_interval = minisrv_config.services['wtv-news'].groupMetaRefreshInterval || 86400;
|
||||
this.data_path = this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore + '/minisrv_internal_nntp');
|
||||
this.createDataStore();
|
||||
|
||||
if (using_auth && (!username && !password)) {
|
||||
// using auth, but no auth info specified, so randomly generate it
|
||||
this.username = this.wtvshared.generateString(8);
|
||||
this.password = this.wtvshared.generatePassword(16);
|
||||
}
|
||||
|
||||
if (run_server) {
|
||||
// nntp-server module overrides
|
||||
var self = this;
|
||||
|
||||
nntp_server.prototype = {
|
||||
...nntp_server.prototype,
|
||||
_authenticate: function (session) {
|
||||
// authenticate
|
||||
if (session.authinfo_user == self.username && session.authinfo_pass == self.password) {
|
||||
session.posting_allowed = true;
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(false);
|
||||
},
|
||||
_postArticle: function (session) {
|
||||
try {
|
||||
session.group.name = self.getHeader(session.post_data, "newsgroups");
|
||||
if (session.group.name.indexOf(',') >= 0) return false; // cross post not implemented
|
||||
return self.postArticle(session.group.name, session.post_data)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_getGroups: function (session, time = 0, wildmat = null) {
|
||||
if (time > 0) return false // unimplemented
|
||||
return self.getGroups(wildmat);
|
||||
},
|
||||
_getLast: function (session) {
|
||||
if (!session.group.name) return nntp_statuses._412_GRP_NOT_SLCTD;
|
||||
if (!session.group.current_article) return nntp_statuses._420_ARTICLE_NOT_SLCTD;
|
||||
if (!self.articleExists(session.group.name, session.group.current_article)) return nntp_statuses._420_ARTICLE_NOT_SLCTD;
|
||||
var res = self.getLastArticle(session.group.name, session.group.current_article);
|
||||
if (!res) return nntp_statuses._422_NO_LAST_ARTICLE;
|
||||
return res;
|
||||
},
|
||||
|
||||
_getNext: function (session) {
|
||||
if (!session.group.name) return nntp_statuses._412_GRP_NOT_SLCTD;
|
||||
if (!session.group.current_article) return nntp_statuses._420_ARTICLE_NOT_SLCTD;
|
||||
if (!self.articleExists(session.group.name, session.group.current_article)) return nntp_statuses._420_ARTICLE_NOT_SLCTD;
|
||||
var res = self.getNextArticle(session.group.name, session.group.current_article);
|
||||
if (!res) return nntp_statuses._421_NO_NEXT_ARTICLE;
|
||||
return res;
|
||||
},
|
||||
|
||||
_selectGroup: function (session, name) {
|
||||
// selectGroup
|
||||
var res = self.selectGroup(name);
|
||||
if (!res.failed) {
|
||||
session.group = res;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
_buildHead: function (session, message) {
|
||||
var out = "";
|
||||
Object.keys(message.headers).forEach((k) => {
|
||||
if (k.length > 0) out += `${k}: ${message.headers[k]}\r\n`;
|
||||
});
|
||||
out = out.substr(0, out.length - 2);
|
||||
return out;
|
||||
},
|
||||
|
||||
_buildHeaderField: function (session, message, field) {
|
||||
if (field.indexOf(':') > 0) field = field.replace(/\:/g, '');
|
||||
var search = self.getHeader(message, field);
|
||||
if (search) return search;
|
||||
else return null;
|
||||
},
|
||||
|
||||
_getOverviewFmt: function (session) {
|
||||
var headers = [
|
||||
"Subject:",
|
||||
"From:",
|
||||
"Date:",
|
||||
"Message-ID:",
|
||||
"References:",
|
||||
":bytes",
|
||||
":lines"
|
||||
]
|
||||
return headers;
|
||||
},
|
||||
_getArticle: function (session, message_id) {
|
||||
// getArticle
|
||||
return new Promise((resolve, reject) => {
|
||||
var res = self.getArticle(session.group.name, message_id);
|
||||
if (!res.messageId) reject(res);
|
||||
else resolve(res)
|
||||
});
|
||||
},
|
||||
|
||||
_buildBody: function (session, message) {
|
||||
return message.body;
|
||||
},
|
||||
|
||||
_getRange: function (session, first, last) {
|
||||
var res = self.listGroup(session.group.name, first, last)
|
||||
if (res.failed) return false;
|
||||
session.group = res.group_data;
|
||||
return res.articles;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var tls_options = {
|
||||
ca: this.wtvshared.getServiceDep('wtv-news/localserver_ca.pem'),
|
||||
key: this.wtvshared.getServiceDep('wtv-news/localserver_key.pem'),
|
||||
cert: this.wtvshared.getServiceDep('wtv-news/localserver_cert.pem'),
|
||||
}
|
||||
this.local_server = new nntp_server({ requireAuth: using_auth, tls: tls_options, secure: true, allow_posting: true });
|
||||
this.local_server.listen('nntps://localhost:' + local_server_port);
|
||||
}
|
||||
}
|
||||
|
||||
getMetaFilename(group) {
|
||||
var g = this.getGroupPath(group);
|
||||
if (g) return g + this.path.sep + "meta.json";
|
||||
else return null;
|
||||
}
|
||||
|
||||
getHeader(message, header) {
|
||||
if (message.headers) {
|
||||
var search = Object.keys(message.headers).find(e => (e.toLowerCase() == header.toLowerCase()));
|
||||
if (search) return message.headers[search];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
createDataStore() {
|
||||
if (!this.fs.existsSync(this.data_path)) return this.fs.mkdirSync(this.data_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
createMetaFile(group, description = null) {
|
||||
const g = this.getMetaFilename(group);
|
||||
if (this.fs.existsSync(g)) return false;
|
||||
var metadata = this.selectGroup(group, true, true);
|
||||
if (description) metadata.description = description;
|
||||
this.saveMetadata(group, metadata, true);
|
||||
return (!metadata.failed) ? metadata : false
|
||||
}
|
||||
|
||||
saveMetadata(group, metadata, creating = false) {
|
||||
const g = this.getMetaFilename(group);
|
||||
if (g) {
|
||||
if (!this.fs.existsSync(g) && !creating) this.createMetaFile(group);
|
||||
else this.fs.writeFileSync(g, JSON.stringify(metadata));
|
||||
} else return false;
|
||||
}
|
||||
|
||||
getMetadata(group) {
|
||||
const g = this.getMetaFilename(group);
|
||||
if (g) {
|
||||
if (this.fs.existsSync(g)) return JSON.parse(this.fs.readFileSync(g));
|
||||
else return false
|
||||
} else return false;
|
||||
}
|
||||
|
||||
findHeaderCaseInsensitive(headers, header) {
|
||||
// returns the key with the found case
|
||||
var response = null;
|
||||
if (headers) {
|
||||
Object.keys(headers).forEach((k) => {
|
||||
if (k.toLowerCase() == header.toLowerCase()) {
|
||||
response = k;
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
postArticle(group, post_data) {
|
||||
var articleNumber = this.getMetadata(group).max_index + 1;
|
||||
if (!articleNumber) return false;
|
||||
try {
|
||||
post_data.articleNumber = articleNumber;
|
||||
post_data.messageId = this.getHeader(post_data, "message-id");
|
||||
if (!post_data.messageId) {
|
||||
var messageId = "<" + this.wtvshared.generateString(16) + "@" + this.minisrv_config.config.domain_name + ">";
|
||||
post_data.messageId = post_data.headers['Message-ID'] = messageId;
|
||||
}
|
||||
|
||||
if (!post_data.headers.Path) post_data.headers.Path = "@" + this.minisrv_config.config.domain_name;
|
||||
if (!post_data.headers.Subject) post_data.headers.Subject = "(No subject)";
|
||||
|
||||
post_data.headers.Date = this.strftime("%a, %-d %b %Y %H:%M:%S %z", Date.parse(post_data.headers.date))
|
||||
|
||||
// server added Injection-Date
|
||||
post_data.headers['Injection-Date'] = this.strftime("%a, %-d %b %Y %H:%M:%S %z", Date.parse(Date.now()))
|
||||
|
||||
// Reorder headers per examples in RFC3977 sect 6.2.1.3, not sure if needed
|
||||
post_data.headers = this.wtvshared.moveObjectElement('Path', null, post_data.headers, true);
|
||||
post_data.headers = this.wtvshared.moveObjectElement('From', 'Path', post_data.headers, true);
|
||||
post_data.headers = this.wtvshared.moveObjectElement('Newsgroups', 'From', post_data.headers, true);
|
||||
post_data.headers = this.wtvshared.moveObjectElement('Subject', 'Newsgroups', post_data.headers, true);
|
||||
post_data.headers = this.wtvshared.moveObjectElement('Date', 'Subject', post_data.headers, true);
|
||||
post_data.headers = this.wtvshared.moveObjectElement('Organization', 'Date', post_data.headers, true);
|
||||
post_data.headers = this.wtvshared.moveObjectElement('Message-ID', 'Organization', post_data.headers, true);
|
||||
// end reordering of headers
|
||||
|
||||
if (this.articleExists(group, post_data.articleNumber)) return false // should not occur, but just in case
|
||||
return this.createArticle(group, post_data.articleNumber, post_data);
|
||||
} catch (e) {
|
||||
console.error(" * WTVNewsServer Error: postArticle: ", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
createArticle(group, articleNumber, article) {
|
||||
var g = this.getGroupPath(group);
|
||||
var file = g + this.path.sep + articleNumber + ".newz";
|
||||
try {
|
||||
this.fs.writeFileSync(file, JSON.stringify(article));
|
||||
var metadata = this.getMetadata(group);
|
||||
metadata.max_index = metadata.max_index + 1;
|
||||
metadata.total = metadata.total + 1;
|
||||
this.saveMetadata(group, metadata)
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(" * WTVNewsServer Error: createArticle: ", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getGroupPath(group) {
|
||||
return this.data_path + this.path.sep + group;
|
||||
}
|
||||
|
||||
getArticlePath(group, article) {
|
||||
return this.getGroupPath(group) + this.path.sep + article + ".newz";
|
||||
}
|
||||
|
||||
articleExists(group, article) {
|
||||
const g = this.getArticlePath(group, article);
|
||||
if (this.fs.existsSync(g)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
createGroup(group, description = null) {
|
||||
var g = this.getGroupPath(group);
|
||||
if (!this.fs.existsSync(g)) {
|
||||
this.fs.mkdirSync(g);
|
||||
return this.createMetaFile(group, description)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
getArticle(group, article) {
|
||||
const g = this.getArticlePath(group, article);
|
||||
if (!this.fs.existsSync(g)) return false;
|
||||
try {
|
||||
var data = JSON.parse(this.fs.readFileSync(g));
|
||||
if (data.article) data = data.article;
|
||||
data.index = data.articleNumber;
|
||||
if (!data.body) data.body = [''];
|
||||
if (!this.findHeaderCaseInsensitive(data.headers,'subject')) data.headers.Subject = "(No subject)";
|
||||
return data
|
||||
} catch (e) {
|
||||
console.error(" * WTVNewsServer Error: getArticle: ", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
selectGroup(group, force_update = false, initial_update = false) {
|
||||
var g = this.getGroupPath(group);
|
||||
var meta = this.getMetadata(group);
|
||||
if (!meta) force_update, initial_update = true;
|
||||
|
||||
if (initial_update) {
|
||||
var out = {
|
||||
total: 0,
|
||||
min_index: 0,
|
||||
max_index: 0,
|
||||
name: group
|
||||
}
|
||||
} else var out = { ...meta }
|
||||
|
||||
if (meta.min_index == 0) force_update = true;
|
||||
if (this.featuredGroups) {
|
||||
Object.keys(this.featuredGroups).forEach((k) => {
|
||||
if (group == this.featuredGroups[k].name) {
|
||||
out.wildmat = 'y';
|
||||
out.description = this.featuredGroups[k].description
|
||||
return false;
|
||||
}
|
||||
})
|
||||
}
|
||||
try {
|
||||
if (force_update || this.doesMetaNeedRefreshing(meta)) {
|
||||
out.total = 0;
|
||||
this.fs.readdirSync(g).forEach(file => {
|
||||
if (file == "meta.json") return;
|
||||
var articleNumber = parseInt(file.split('.')[0]);
|
||||
if (out.min_index == 0) out.min_index = articleNumber;
|
||||
else if (articleNumber < out.min_index) out.min_index = articleNumber;
|
||||
else if (articleNumber > out.max_index) out.max_index = articleNumber;
|
||||
|
||||
|
||||
out.total++;
|
||||
});
|
||||
if (initial_update) {
|
||||
out.last_scan = Math.floor(Date.now() / 1000);
|
||||
} else {
|
||||
meta = { ...meta, ...out }
|
||||
if (meta.wildmat) delete meta.wildmat;
|
||||
meta.last_scan = Math.floor(Date.now() / 1000);
|
||||
this.saveMetadata(group, meta);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
out.failed = e;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
getGroups(wildmat = null) {
|
||||
var groups = [];
|
||||
this.fs.readdirSync(this.data_path).forEach(file => {
|
||||
if (this.fs.lstatSync(this.data_path + this.path.sep + file).isDirectory()) {
|
||||
if (wildmat) {
|
||||
if (file.match(wildmat)) groups.push(this.selectGroup(file));
|
||||
} else groups.push(this.selectGroup(file));
|
||||
}
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
getLastArticle(group, current) {
|
||||
var g = this.getGroupPath(group);
|
||||
var res = null;
|
||||
try {
|
||||
var articleNumbers = [];
|
||||
this.fs.readdirSync(g).forEach(file => {
|
||||
if (file == "meta.json") return;
|
||||
var articleNumber = parseInt(file.split('.')[0]);
|
||||
articleNumbers.push(articleNumber);
|
||||
});
|
||||
articleNumbers.sort((a, b) => a - b)
|
||||
var index = articleNumbers.findIndex((e) => e == current) - 1;
|
||||
if (index >= 0) res = articleNumbers[index];
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
if (res) {
|
||||
if (res == current) return null;
|
||||
var message = this.getArticle(group, res);
|
||||
if (message.messageId) {
|
||||
res = { "articleNumber": res, "message_id": message.messageId };
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
getNextArticle(group, current) {
|
||||
var g = this.getGroupPath(group);
|
||||
var res = null;
|
||||
try {
|
||||
var articleNumbers = [];
|
||||
this.fs.readdirSync(g).forEach(file => {
|
||||
if (file == "meta.json") return;
|
||||
var articleNumber = parseInt(file.split('.')[0]);
|
||||
articleNumbers.push(articleNumber);
|
||||
});
|
||||
articleNumbers.sort((a, b) => a - b)
|
||||
var index = articleNumbers.findIndex((e) => e == current) + 1;
|
||||
if (index < articleNumbers.length) res = articleNumbers[index];
|
||||
} catch (e) {
|
||||
return e;
|
||||
}
|
||||
if (res) {
|
||||
var message = this.getArticle(group, res);
|
||||
if (message.messageId) {
|
||||
res = { "articleNumber": res, "message_id": message.messageId };
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
doesMetaNeedRefreshing(meta) {
|
||||
if (!meta) return true;
|
||||
if (!meta.max_index) return true;
|
||||
if (!meta.min_index) return true;
|
||||
if (!meta.total) return true;
|
||||
if (!meta.last_scan) return true;
|
||||
if (meta.last_scan) {
|
||||
if ((Math.floor(Date.now() / 1000) - this.scan_interval) > meta.last_scan) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
listGroup(group, start, end, force_update = false) {
|
||||
var g = this.getGroupPath(group);
|
||||
var out = {
|
||||
total: 0,
|
||||
min_index: 0,
|
||||
max_index: 0,
|
||||
name: group
|
||||
}
|
||||
var articles = [];
|
||||
try {
|
||||
var meta = this.getMetadata(group);
|
||||
this.fs.readdirSync(g).forEach(file => {
|
||||
if (file == "meta.json") return;
|
||||
var articleNumber = parseInt(file.split('.')[0]);
|
||||
if (articleNumber < start) return;
|
||||
if (articleNumber > end) return false;
|
||||
if (out.min_index == null) out.min_index = articleNumber;
|
||||
else if (articleNumber < out.min_index) out.min_index = articleNumber;
|
||||
|
||||
if (articleNumber > out.max_index) out.max_index = articleNumber;
|
||||
articles.push(this.getArticle(group, articleNumber));
|
||||
out.total++;
|
||||
});
|
||||
if (force_update || this.doesMetaNeedRefreshing(meta)) {
|
||||
meta = { ...meta, ...out }
|
||||
meta.last_scan = Math.floor(Date.now() / 1000);
|
||||
this.saveMetadata(group, meta);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(" * WTVNewsServer Error: listGroup: ", e);
|
||||
out.failed = e;
|
||||
}
|
||||
articles.sort((a, b) => a.index - b.index)
|
||||
if (out.min_index === null) out.min_index = 0;
|
||||
return {
|
||||
articles: articles,
|
||||
group_data: meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WTVNewsServer;
|
||||
198
zefie_wtvp_minisrv/includes/classes/WTVRegister.js
Normal file
198
zefie_wtvp_minisrv/includes/classes/WTVRegister.js
Normal file
@@ -0,0 +1,198 @@
|
||||
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";
|
||||
this.session_store_dir = session_store_dir || this.minisrv_config.config.SessionStore;
|
||||
}
|
||||
|
||||
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);
|
||||
return regex.test(username);
|
||||
}
|
||||
|
||||
|
||||
checkUsernameAvailable(username, directory = null) {
|
||||
var self = this;
|
||||
var return_val = false;
|
||||
// returns the user's ssid, and user_id and userid in an array if true, false if not
|
||||
|
||||
// check against reserved name list
|
||||
if (this.minisrv_config.config.user_accounts.reserved_names) {
|
||||
Object.keys(this.minisrv_config.config.user_accounts.reserved_names).forEach((k) => {
|
||||
if (self.minisrv_config.config.user_accounts.reserved_names[k].toLowerCase() == username.toLowerCase()) return_val = true;
|
||||
})
|
||||
}
|
||||
|
||||
if (return_val) return !return_val;
|
||||
|
||||
// check against user accounts
|
||||
directory = (directory) ? directory : this.session_store_dir + this.path.sep + "accounts";
|
||||
|
||||
if (this.fs.existsSync(directory)) {
|
||||
this.fs.readdirSync(directory).forEach(file => {
|
||||
if (self.fs.lstatSync(directory + self.path.sep + file).isDirectory() && !return_val) {
|
||||
return_val = !self.checkUsernameAvailable(username, directory + self.path.sep + file);
|
||||
}
|
||||
if (!file.match(/user.*\.json/ig)) return;
|
||||
try {
|
||||
var temp_session_data_file = self.fs.readFileSync(directory + self.path.sep + file, 'Utf8');
|
||||
var temp_session_data = JSON.parse(temp_session_data_file);
|
||||
if (temp_session_data.subscriber_username) {
|
||||
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>
|
||||
|
||||
</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;
|
||||
396
zefie_wtvp_minisrv/includes/classes/WTVSec.js
Normal file
396
zefie_wtvp_minisrv/includes/classes/WTVSec.js
Normal 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;
|
||||
1054
zefie_wtvp_minisrv/includes/classes/WTVShared.js
Normal file
1054
zefie_wtvp_minisrv/includes/classes/WTVShared.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user