comments, move functions, scrapbook progress

This commit is contained in:
zefie
2025-07-21 14:35:56 -04:00
parent e2b4aca277
commit 90522cc796
16 changed files with 756 additions and 532 deletions

View File

@@ -1,3 +1,5 @@
const WTVClientSessionData = require('./WTVClientSessionData.js');
class WTVAdmin {
fs = require('fs');
@@ -18,6 +20,12 @@ class WTVAdmin {
REASON_EXISTS = 4
REASON_NONEXIST = 5
/**
* Creates an instance of WTVAdmin.
* @param {Object} minisrv_config
* @param {WTVClientSessionData} wtvclient
* @param {string} service_name
*/
constructor(minisrv_config, wtvclient, service_name) {
this.minisrv_config = minisrv_config;
var { WTVShared } = require("./WTVShared.js");
@@ -36,6 +44,12 @@ class WTVAdmin {
this.service_name = service_name;
}
/**
* Bans a specific SSID.
* @param {string} ssid The SSID to ban
* @param {string} admin_ssid The SSID of the admin requesting the ban
* @returns {number} The result of the ban operation
*/
banSSID(ssid, admin_ssid = null) {
if (ssid == admin_ssid) {
return this.REASON_NOSELF;
@@ -58,22 +72,27 @@ class WTVAdmin {
}
}
/**
* Unbans a specific SSID.
* @param {string} ssid The SSID to unban
* @returns {number} The result of the unban operation
*/
unbanSSID(ssid) {
var config_changed = false;
var fake_config = wtvshared.getUserConfig();
var fake_config = this.wtvshared.getUserConfig();
if (!fake_config.config) fake_config.config = {};
if (!fake_config.config.ssid_block_list) fake_config.config.ssid_block_list = [];
if (typeof request_headers.query.ssid === 'string') {
if (typeof ssid === 'string') {
Object.keys(fake_config.config.ssid_block_list).forEach(function (k) {
if (fake_config.config.ssid_block_list[k].toLowerCase() == request_headers.query.ssid.toLowerCase()) {
if (fake_config.config.ssid_block_list[k].toLowerCase() == ssid.toLowerCase()) {
fake_config.config.ssid_block_list.splice(k, 1);
config_changed = true
}
});
} else {
Object.keys(fake_config.config.ssid_block_list).forEach(function (k) {
Object.keys(request_headers.query.ssid).forEach(function (j) {
if (fake_config.config.ssid_block_list[k].toLowerCase() == request_headers.query.ssid[j].toLowerCase()) {
Object.keys(ssid).forEach(function (j) {
if (fake_config.config.ssid_block_list[k].toLowerCase() == ssid[j].toLowerCase()) {
fake_config.config.ssid_block_list.splice(k, 1);
config_changed = true
}
@@ -89,38 +108,11 @@ class WTVAdmin {
}
}
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 is given as a single IP address (no CIDR notation)
if (subnet.indexOf('/') === -1) {
return this.ip2long(ip) === this.ip2long(subnet);
} else {
// Expect subnet in format "base_ip/prefix_length"
let parts = subnet.match(/^(.*?)\/(\d{1,2})$/);
if (parts && (this.ip2long(parts[1]) >= 0)) {
let base_ip = this.ip2long(parts[1]);
let prefixLength = parseInt(parts[2]);
let freedom = Math.pow(2, 32 - prefixLength);
return (this.ip2long(ip) >= base_ip) && (this.ip2long(ip) < base_ip + freedom);
}
}
return false;
}
/**
* Rejects a connection attempt based on the client's address or SSID.
* @param {boolean} reason_is_ssid If true, the rejection is based on SSID, otherwise on IP address
* @returns {string} The reason for rejecting the connection
*/
rejectConnection(reason_is_ssid) {
var rejectReason;
if (this.pcservices) {
@@ -138,6 +130,11 @@ class WTVAdmin {
return rejectReason;
}
/**
* Checks if the provided password matches the service's password.
* @param {string} password The password to check
* @returns {boolean} True if the password matches, false otherwise
*/
checkPassword(password) {
if (this.pcservices) {
if (this.minisrv_config.config.pc_admin.password) {
@@ -156,6 +153,10 @@ class WTVAdmin {
}
}
/**
* Lists all registered SSIDs.
* @returns {Array} An array of arrays, each containing the SSID and its associated account information
*/
listRegisteredSSIDs() {
var search_dir = this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore + this.path.sep + "accounts");
var self = this;
@@ -169,6 +170,11 @@ class WTVAdmin {
return out;
}
/**
* Checks if the current client is authorized to access the service.
* @param {boolean} justchecking If true, only checks authorization without rejecting the connection
* @return {boolean} True if authorized, false otherwise
*/
isAuthorized(justchecking = false) {
var allowed_ssid = false;
var allowed_ip = false;
@@ -186,7 +192,7 @@ class WTVAdmin {
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])) {
if (self.wtvshared.isInSubnet(self.clientAddress, self.minisrv_config.services[self.service_name].authorized_ssids[k][j])) {
if (allowed_ip) return;
allowed_ip = true;
}
@@ -206,7 +212,7 @@ class WTVAdmin {
var self = this;
Object.keys(this.minisrv_config.config.pc_admin.ip_whitelist).forEach(function (k) {
if (allowed_ip) return;
allowed_ip = self.isInSubnet(self.clientAddress, self.minisrv_config.config.pc_admin.ip_whitelist[k]);
allowed_ip = self.wtvshared.isInSubnet(self.clientAddress, self.minisrv_config.config.pc_admin.ip_whitelist[k]);
});
}
}
@@ -219,6 +225,12 @@ class WTVAdmin {
}
}
/**
* Gets the account information for a specific username.
* @param {string} username The username to get the account information for
* @param {string|null} directory The directory to search for user accounts, defaults to the session store directory
* @returns {Object|null} An object containing account information if the username is found, null otherwise
*/
getAccountInfo(username, directory = null) {
var search_dir = this.wtvshared.getAbsolutePath(this.minisrv_config.config.SessionStore + this.path.sep + "accounts");
var account_data = null;
@@ -255,6 +267,11 @@ class WTVAdmin {
return null;
}
/**
* Gets the account information for a specific SSID.
* @param {string} ssid The SSID to get the account information for
* @returns {Object|boolean} An object containing account information if the SSID is registered, false otherwise
*/
getAccountInfoBySSID(ssid) {
var account_info = {};
var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid);
@@ -278,13 +295,22 @@ class WTVAdmin {
else return false;
}
/**
* Gets the account session data for a specific SSID.
* @param {string} ssid The SSID to get the account data for
* @returns {WTVClientSessionData} The session data object for the account
*/
getAccountBySSID(ssid) {
var userSession = new this.WTVClientSessionData(this.minisrv_config, ssid);
userSession.user_id = 0;
return userSession;
}
/**
* Checks if a specific SSID is banned.
* @param {string} ssid The SSID to check
* @returns {boolean} True if the SSID is banned, false otherwise
*/
isBanned(ssid) {
var self = this;
var isBanned = false;

View File

@@ -10,7 +10,6 @@ class WTVAuthor {
wtvclient = null;
pageFileExt = ".page";
pagestore_dir = null;
scrapbook_dir = null;
pageArr = [];
blockArr = [];
header = null;
@@ -68,18 +67,6 @@ class WTVAuthor {
}
return true;
}
scrapbookExists() {
if (!this.isguest) {
if (this.scrapbook_dir === null) {
var userstore_dir = this.wtvclient.getUserStoreDirectory();
var store_dir = "Scrapbook" + this.path.sep;
this.scrapbook_dir = userstore_dir + store_dir;
}
}
return this.fs.existsSync(this.scrapbook_dir);
}
createPagestore() {
if (this.pagestoreExists() === false) {
@@ -91,92 +78,6 @@ class WTVAuthor {
return false;
}
createScrapbook() {
if (this.scrapbookExists() === false) {
try {
if (!this.fs.existsSync(this.scrapbook_dir)) this.fs.mkdirSync(this.scrapbook_dir, { recursive: true });
return true;
} catch { }
}
return false
}
scrapbookDir() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
return this.scrapbook_dir;
}
listScrapbook() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
const files = this.fs.readdirSync(this.scrapbook_dir);
const filteredFiles = files.filter(file => !file.endsWith('.meta'));
return filteredFiles;
}
getFreeScrapbookID() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var id = 1;
var files = this.fs.readdirSync(this.scrapbook_dir);
if (files.length == 0) {
return id;
}
files = files.map(file => parseInt(file.substr(0, file.indexOf('.'))));
while (files.includes(id)) {
id++;
}
return id;
}
getScrapbookImage(id) {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var file = this.scrapbook_dir + id;
if (this.fs.existsSync(file)) {
return this.fs.readFileSync(file);
}
return null;
}
getScrapbookImageType(id) {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var file = this.scrapbook_dir + id + ".meta";
if (this.fs.existsSync(file)) {
var meta = this.fs.readFileSync(file, 'utf8');
try {
var metaData = JSON.parse(meta);
return metaData.contentType;
} catch (e) {
this.debug("getScrapbookImageType", "Error parsing metadata for image ID", id, e);
}
}
return null;
}
addToScrapbook(filename, contentType, data) {
try {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var fileout = this.scrapbook_dir + filename;
var fileout_meta = this.scrapbook_dir + filename + ".meta";
this.fs.writeFileSync(fileout, data);
this.fs.writeFileSync(fileout_meta, JSON.stringify({
"contentType": contentType
}));
return true;
} catch {}
return false;
}
createPage(style) {
this.pagestoreExists()
var pagestorepath = this.pagestore_dir;

View File

@@ -16,6 +16,7 @@ class WTVClientSessionData {
mailstore = null;
favstore = null;
pagestore = null;
scrapbook_dir = null;
login_security = null;
capabilities = null;
session_storage = "";
@@ -108,6 +109,12 @@ class WTVClientSessionData {
}
}
/**
* Sets a ticket data value.
* @param {string} key The key of the ticket data
* @param {*} value The value to set
* @returns {boolean} True if the value was set successfully, false otherwise
*/
setTicketData(key, value) {
if (this.data_store.wtvsec_login) this.data_store.wtvsec_login.setTicketData(key, value);
else return false;
@@ -115,16 +122,24 @@ class WTVClientSessionData {
return true;
}
/**
* Retrieves ticket data by key.
* @param {string} key The key of the ticket data
* @return {*} The value associated with the key, or false if not found
*/
getTicketData(key) {
if (this.data_store.wtvsec_login) return this.data_store.wtvsec_login.getTicketData(key);
return false;
}
/**
* Deletes ticket data by key.
* @param {string} key The key of the ticket data to delete
* @return {boolean} True if the data was deleted successfully, false otherwise
*/
deleteTicketData(key) {
if (this.data_store.wtvsec_login) this.data_store.wtvsec_login.deleteTicketData(key);
else return false;
return true;
}
@@ -328,6 +343,136 @@ class WTVClientSessionData {
return (result === false) ? false : true;
}
scrapbookExists() {
if (!this.isguest) {
if (this.scrapbook_dir === null) {
var userstore_dir = this.getUserStoreDirectory();
var store_dir = "Scrapbook" + this.path.sep;
this.scrapbook_dir = userstore_dir + store_dir;
}
}
return this.fs.existsSync(this.scrapbook_dir);
}
createScrapbook() {
if (this.scrapbookExists() === false) {
try {
if (!this.fs.existsSync(this.scrapbook_dir)) this.fs.mkdirSync(this.scrapbook_dir, { recursive: true });
return true;
} catch { }
}
return false
}
scrapbookDir() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
return this.scrapbook_dir;
}
listScrapbook() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
const files = this.fs.readdirSync(this.scrapbook_dir);
const filteredFiles = files.sort(function(a, b) {
return a.localeCompare(b, undefined, {
numeric: true,
sensitivity: 'base'
});
}).filter(file => !file.endsWith('.meta'));
return filteredFiles;
}
getFreeScrapbookID() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var id = 1;
var files = this.fs.readdirSync(this.scrapbook_dir);
if (files.length == 0) {
return id;
}
files = files.map(file => parseInt(file.substr(0, file.indexOf('.'))));
while (files.includes(id)) {
id++;
}
return id;
}
getScrapbookUsage() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var total_size = 0;
var files = this.fs.readdirSync(this.scrapbook_dir);
files.forEach(file => {
if (!file.endsWith('.meta')) {
var file_path = this.scrapbook_dir + file;
if (this.fs.existsSync(file_path)) {
total_size += this.fs.statSync(file_path).size;
}
}
});
return total_size;
}
getScrapbookUsagePercent() {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var total_size = this.getScrapbookUsage();
var max_size = this.minisrv_config.config.user_accounts.scrapbook_storage * 1024 * 1024; // convert to bytes
if (max_size <= 0) return 0; // no storage limit set
var usage_percent = (total_size / max_size) * 100;
return Math.round(usage_percent, 2);
}
getScrapbookImage(id) {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var file = this.scrapbook_dir + id;
if (this.fs.existsSync(file)) {
return this.fs.readFileSync(file);
}
return null;
}
getScrapbookImageType(id) {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var file = this.scrapbook_dir + id + ".meta";
if (this.fs.existsSync(file)) {
var meta = this.fs.readFileSync(file, 'utf8');
try {
var metaData = JSON.parse(meta);
return metaData.contentType;
} catch (e) {
this.debug("getScrapbookImageType", "Error parsing metadata for image ID", id, e);
}
}
return null;
}
addToScrapbook(filename, contentType, data) {
try {
if (this.scrapbookExists() === false) {
this.createScrapbook();
}
var fileout = this.scrapbook_dir + filename;
var fileout_meta = this.scrapbook_dir + filename + ".meta";
this.fs.writeFileSync(fileout, data);
this.fs.writeFileSync(fileout_meta, JSON.stringify({
"contentType": contentType
}));
return true;
} catch {}
return false;
}
/**
* Retrieves a file from the user store
* @param {string} path Path relative to the User File Store
@@ -373,12 +518,23 @@ class WTVClientSessionData {
return Object.keys(this.session_store.cookies).length || 0;
}
/**
* Resets the user cookies
*/
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");
}
/**
* Adds a cookie to the user's session store
* @param {string|object} domain Domain for the cookie, or an object with cookie data
* @param {string|null} path Path for the cookie, defaults to null
* @param {string|null} expires Expiration date for the cookie, defaults to null
* @param {string|null} data Data for the cookie, defaults to null
* @return {boolean} True if the cookie was added successfully, false otherwise
*/
addCookie(domain, path = null, expires = null, data = null) {
if (!this.checkCookies()) this.resetCookies();
if (!domain) return false;
@@ -413,6 +569,12 @@ class WTVClientSessionData {
return true;
}
/**
* Retrieves a cookie from the user's session store
* @param {string} domain Domain of the cookie
* @param {string} path Path of the cookie
* @return {object|false} Cookie data if found, false otherwise
*/
getCookie(domain, path) {
if (!this.checkCookies()) this.resetCookies();
var self = this;
@@ -431,6 +593,12 @@ class WTVClientSessionData {
return result;
}
/**
* Retrieves a cookie string from the user's session store
* @param {string} domain Domain of the cookie
* @param {string} path Path of the cookie
* @return {string|false} Cookie string if found, false otherwise
*/
getCookieString(domain, path) {
var cookie_data = this.getCookie(domain, path);
/*
@@ -443,6 +611,12 @@ class WTVClientSessionData {
return cookie_data.cookie;
}
/**
* Deletes a cookie from the user's session store
* @param {string|object} domain Domain of the cookie, or an object with cookie data
* @param {string|null} path Path of the cookie, defaults to null
* @return {boolean} True if the cookie was deleted successfully, false otherwise
*/
deleteCookie(domain, path = null) {
var result = false;
if (!this.checkCookies()) {
@@ -472,12 +646,20 @@ class WTVClientSessionData {
return result;
}
/**
* Checks if there are any cookies stored in the session
* @return {boolean} True if there are cookies, false otherwise
*/
checkCookies() {
if (!this.session_store.cookies) return false;
else if (this.session_store.cookies == []) return false;
return true;
}
/**
* Lists all cookies in the user's session store
* @return {string} String representation of all cookies, each cookie separated by a null character
*/
listCookies() {
if (!this.checkCookies()) this.resetCookies();
var outstring = "";

View File

@@ -1,177 +0,0 @@
class WTVPCAdmin {
fs = require('fs');
path = require('path');
minisrv_config = [];
wtvr = null;
wtvshared = null;
socket = null;
WTVClientSessionData = require("./WTVClientSessionData.js");
service_name = "wtv-admin";
constructor(minisrv_config, socket, service_name) {
this.minisrv_config = minisrv_config;
var { WTVShared } = require("./WTVShared.js");
var WTVRegister = require("./WTVRegister.js");
this.socket = socket;
this.wtvr = new WTVRegister(minisrv_config);
this.clientAddress = socket.remoteAddress;
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() {
var rejectReason;
rejectReason = this.clientAddress + " is not in the whitelist.";
console.log(" * Request from IP", this.clientAddress, "for PC Services Admin, but that IP is not authorized.");
return rejectReason;
}
checkPassword(password) {
if (this.minisrv_config.config.pc_admin.password) {
return (password == this.minisrv_config.config.pc_admin.password);
} else {
// no password set
return true;
}
}
isAuthorized(justchecking = false) {
var allowed_ip = false;
if (this.minisrv_config.config.pc_admin.ip_whitelist) {
var self = this;
Object.keys(this.minisrv_config.config.pc_admin.ip_whitelist).forEach(function (k) {
allowed_ip = self.isInSubnet(self.clientAddress, self.minisrv_config.config.pc_admin.ip_whitelist[k]);
});
}
if (justchecking) {
return allowed_ip;
} else {
return allowed_ip ? true : this.rejectConnection();
}
}
listRegisteredSSIDs() {
var search_dir = this.minisrv_config.config.SessionStore + this.path.sep + "accounts";
var self = this;
var out = [];
this.fs.readdirSync(search_dir).forEach(file => {
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory()) {
var user = self.getAccountInfoBySSID(file);
out.push([file,user]);
}
});
return out;
}
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();
if (account_info.account_users) {
if (account_info.account_users['subscriber']) {
account_info.username = account_info.account_users['subscriber'].subscriber_username;
} else {
account_info.username = account_info.account_users[0];
}
} else {
account_info.username = account_info.account_users[0];
}
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 = WTVPCAdmin;

View File

@@ -21,12 +21,22 @@ class WTVRegister {
}
}
/**
* Checks if the username is valid according to the configured rules.
* @param {string} username The username to check
* @returns {boolean} True if the username is valid, false otherwise
*/
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);
}
/**
* Checks if the SSID is already registered.
* @param {string} ssid The SSID to check
* @returns {boolean} True if the SSID is available for registration, false if it already has an account registered.
*/
checkSSIDAvailable(ssid) {
var directory = (directory) ? directory : this.session_store_dir + this.path.sep + "accounts";
var available = true;
@@ -41,6 +51,12 @@ class WTVRegister {
return available;
}
/**
* Checks if the username is already taken.
* @param {string} username The username to check
* @param {string} directory The directory to search for user accounts
* @returns {boolean} True if the username is available, false if it is already taken
*/
checkUsernameAvailable(username, directory = null) {
var self = this;
var return_val = false;

View File

@@ -7,7 +7,7 @@ class WTVSSL {
getCACert() {
// return the CA cert
const caCertFile = this.wtvshared.getServiceDep("https/ca.der")
const caCertFile = this.wtvshared.getServiceDep("https/ca.der", true)
if (!this.wtvshared.fs.existsSync(caCertFile)) {
throw new Error("CA certificate file not found");
}

View File

@@ -2,6 +2,7 @@ const CryptoJS = require('crypto-js');
const endianness = require('endianness');
var RC4 = require('rc4-crypto');
var crypto = require('crypto');
var WTVShared = require("./WTVShared.js")['WTVShared'];
/**
* Javascript implementation of WTVP Security
@@ -34,6 +35,7 @@ class WTVSec {
RC4Session = new Array();
minisrv_config = [];
update_ticket = false;
wtvshared = null;
ticket_store = {};
/**
@@ -46,6 +48,7 @@ class WTVSec {
*/
constructor(minisrv_config, wtv_incarnation = 1) {
this.minisrv_config = minisrv_config;
this.wtvshared = new WTVShared(minisrv_config);
this.initial_shared_key = CryptoJS.enc.Base64.parse(this.minisrv_config.config.keys.initial_shared_key);
if (this.initial_shared_key.sigBytes === 8) {
@@ -58,7 +61,6 @@ class WTVSec {
/**
* Set the wtv-incarnation for this instance
*
* @param {Number} wtv_incarnation
*/
set_incarnation(wtv_incarnation) {
@@ -77,16 +79,16 @@ class WTVSec {
/**
* Clones a WordArray to allow modification without referencing its original
* @param {CryptoJS.lib.WordArray} wa
*
* @param {CryptoJS.lib.WordArray} wordArray
* @returns {CryptoJS.lib.WordArray}
*/
DuplicateWordArray(wa) {
return CryptoJS.lib.WordArray.create(this.wordArrayToBuffer(wa));
DuplicateWordArray(wordArray) {
return CryptoJS.lib.WordArray.create(this.wordArrayToBuffer(wordArray));
}
/**
* Prepares the wtv-ticket for this instance
* @returns {Base64} wtv-ticket
*/
PrepareTicket() {
// store last challenge response in ticket
@@ -110,20 +112,8 @@ class WTVSec {
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) {
@@ -146,7 +136,7 @@ class WTVSec {
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);
this.ticket_store = this.wtvshared.tryDecodeJSON(ticket_data_dec);
} else {
this.ticket_store = {};
}
@@ -155,6 +145,11 @@ class WTVSec {
if (this.minisrv_config.config.debug_flags.debug) console.log(" * Decoded session from wtv-ticket with ticket_store:", this.ticket_store);
}
/**
* Gets the ticket data for this instance
* @param {string} key The key of the ticket data to retrieve
* @returns {any} The ticket data for the specified key, or null if not found
*/
getTicketData(key = null) {
if (typeof (this.ticket_store) === 'session_store') return null;
else if (key === null) return this.ticket_store;
@@ -162,6 +157,11 @@ class WTVSec {
else return null;
}
/**
* Sets the ticket data for this instance
* @param {string} key The key of the ticket data to set
* @param {any} value The value to set for the specified key
*/
setTicketData(key, value) {
if (key === null) throw ("WTVSec.setTicketData(): invalid key provided");
if (typeof (this.ticket_store) === 'undefined') this.ticket_store = {};
@@ -170,6 +170,10 @@ class WTVSec {
this.update_ticket = true;
}
/**
* Deletes the ticket data for this instance
* @param {string} key The key of the ticket data to delete
*/
deleteTicketData(key) {
if (key === null) throw ("WTVSec.deleteTicketData(): invalid key provided");
if (typeof (this.ticket_store) === 'undefined') {
@@ -234,7 +238,6 @@ class WTVSec {
/**
* Generates a wtv-challenge for this instance
*
* @returns {Base64} wtv-challenge
*/
IssueChallenge() {
@@ -246,7 +249,7 @@ class WTVSec {
* 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
* bytes 104 - 112: padding. seemingly not important, but by default is 8 bytes of 0x08
*/
const challenge_id = CryptoJS.lib.WordArray.random(8);
const echo_me = CryptoJS.lib.WordArray.random(40);
@@ -280,8 +283,7 @@ class WTVSec {
/**
* convert a CryptoJS.lib.WordArray to a Javascript Buffer
* @param {CryptoJS.lib.WordArray} wordArray
*
* #returns {Buffer} JS Buffer object
* @returns {Buffer} JS Buffer object
*/
wordArrayToBuffer(wordArray) {
if (wordArray) return new Buffer.from(wordArray.toString(CryptoJS.enc.Hex), 'hex');
@@ -291,7 +293,6 @@ class WTVSec {
/**
* Starts an encryption session
* @param {Number} rc4session Session Type (0 = enc k1, 1 = dec k1, 2 = enc k2, 3 = 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);
@@ -326,7 +327,6 @@ class WTVSec {
* 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) {
@@ -357,8 +357,8 @@ class WTVSec {
* 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
* @notice This function is an alias for Encrypt, as WTVSec uses the same method for both encryption and decryption.
*/
Decrypt(keynum, data) {
return this.Encrypt(keynum, data)

View File

@@ -23,6 +23,12 @@ class WTVShared {
minisrv_config = [];
/**
* Constructor for WTVShared class
* @param {object} minisrv_config The configuration object for the minisrv
* @param {boolean} quiet If true, suppresses console output
* @notice If minisrv_config is null, it will attempt to read the configuration from the minisrv_config.json file
* */
constructor(minisrv_config, quiet = false) {
if (minisrv_config == null) this.minisrv_config = this.readMiniSrvConfig(true, !quiet);
else this.minisrv_config = minisrv_config;
@@ -47,7 +53,120 @@ class WTVShared {
}
}
/**
* Converts an IP address to a hexadecimal string (WTV)
* @param {string} ip The IP address to convert
* @returns {string} The hexadecimal representation of the IP address
* @throws {Error} If the IP address is invalid
*/
ipToHex(ip) {
const parts = ip.split('.');
if (parts.length !== 4) {
throw new Error('Invalid IP address');
}
let num = 0;
for (let i = 0; i < 4; i++) {
const part = parseInt(parts[i], 10);
if (part < 0 || part > 255) {
throw new Error('Invalid IP address');
}
num = (num << 8) | part;
}
// Convert to unsigned 32-bit number before converting to hex
return "0x" + (num >>> 0).toString(16).toUpperCase();
}
/**
* Converts an IP address to a long integer
* @param {string} ip The IP address to convert
* @returns {number} The long integer representation of the IP address, or -1 if the IP address is invalid
*/
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;
}
/**
* Checks if an IP address is in a given subnet.
* @param {string} ip The IP address to check
* @param {string} subnet The subnet in CIDR notation
* @returns {boolean} True if the IP address is in the subnet, false otherwise
*/
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;
}
/**
* Converts a byte array to a 32-bit unsigned integer (big-endian)
* @param {Uint8Array} bytes The byte array
* @param {number} offset The offset within the byte array
* @returns {number} The 32-bit unsigned integer
*/
toUint32(bytes, offset) {
return (
(bytes[offset] << 24) >>> 0 |
(bytes[offset + 1] << 16) |
(bytes[offset + 2] << 8) |
(bytes[offset + 3])
) >>> 0;
}
/**
* Converts a 32-bit unsigned integer to a byte array (big-endian)
* @param {number} num The 32-bit unsigned integer
* @returns {number[]} The byte array representation
* @notice The output is an array of 4 bytes, each byte is an unsigned integer (0-255)
*/
uint32ToBytes(num) {
return [
(num >>> 24) & 0xff,
(num >>> 16) & 0xff,
(num >>> 8) & 0xff,
num & 0xff,
];
}
/**
* Tries to decode a JSON string
* @param {String} json_string
* @returns {Object} The decoded JSON object, or an empty object if decoding fails.
*/
tryDecodeJSON(json_string) {
var out;
try {
out = JSON.parse(json_string);
} catch (e) {
console.log(e);
out = {};
}
return out;
}
/**
* Gets the box name based on the client ROM type
* @param {string} client_rom_type The client ROM type
* @returns {string} The box name
*/
getBoxName(client_rom_type) {
switch (client_rom_type) {
case "bf0app":
@@ -94,14 +213,22 @@ class WTVShared {
return crc.toString(16).padStart(2, '0');
}
// check if the SSID has a valid checksum
/**
* Checks if the SSID has a valid checksum
* @param {string} ssid The SSID to check
* @return {boolean} true if the SSID is valid, false if not
*/
checkSSID(ssid) {
if (ssid.slice(-2) == this.getSSIDCRC(ssid))
return true;
return false;
}
/**
* Parses variables in a string, replacing %ServiceDeps% with the service dependencies
* @param {string} s The string to parse
* @returns {string} The parsed string
*/
parseConfigVars(s) {
if (s.indexOf("%ServiceDeps%") >= 0)
return this.getServiceDep(s.replace("%ServiceDeps%", ""), true);
@@ -155,7 +282,6 @@ class WTVShared {
return src;
}
/**
* Checks if the user has been whitelisted for wtv-admin
* @param {object} wtvclient the clientSessionData object for the user
@@ -169,6 +295,11 @@ class WTVShared {
return result;
}
/**
* Parses a JSON string, removing unsupported comments
* @param {string} json JSON string to parse
* @returns {object} Parsed JSON object
*/
parseJSON(json) {
if (typeof json !== 'string') json = json ? json.toString() : '';
let result = '';
@@ -206,7 +337,6 @@ class WTVShared {
return JSON.parse(result);
}
/**
* Attempts to convert val into a boolean
* @param {string,int,boolean} val
@@ -258,7 +388,6 @@ class WTVShared {
return entitized;
}
/**
* Attempts to sanitize HTML code to remove possible exploits when embedded in a WebTV Service
* @param {string} string The string to sanitize
@@ -332,7 +461,7 @@ class WTVShared {
* @param {string} headers Header string to convert
* @param {boolean} response If true, the headers are a response, otherwise they are a request
* @return {object} Headers object
* */
*/
headerStringToObj(headers, response = false) {
var inc_headers = 0;
var headers_obj = {};
@@ -421,7 +550,6 @@ class WTVShared {
return typeof str === 'string' && /^[\x00-\x7F]*$/.test(str);
}
/**
* Attempts to determine if the string contains HTML
* @param {string} str
@@ -432,7 +560,6 @@ class WTVShared {
return typeof str === 'string' && pattern.test(str);
}
/**
* Attempts to determine if the string is Base64 or not
* @param {string} str String to check
@@ -455,7 +582,11 @@ class WTVShared {
return new RegExp(regex, 'gi').test(str);
}
/**
* Decodes a UTF-8 string to a regular string
* @param {string} utf8String The UTF-8 encoded string to decode
* @returns {string} The decoded string
* */
utf8Decode(utf8String) {
if (typeof utf8String !== 'string') {
throw new TypeError("parameter 'utf8String' is not a string");
@@ -465,7 +596,11 @@ class WTVShared {
return textDecoder.decode(bytes);
}
/**
* Decodes a buffer containing ISO-8859-1 encoded text to a UTF-8 string
* @param {Buffer} buf The buffer to decode
* @returns {string} The decoded string
*/
decodeBufferText(buf) {
var out = "";
out = this.utf8Decode(this.iconv.decode(Buffer.from(buf),'ISO-8859-1'));
@@ -487,7 +622,6 @@ class WTVShared {
return this.fixPathSlashes(check_path);
}
/**
* Detects if the client is in MiniBrowser mode
* @param {object} ssid_session
@@ -507,6 +641,12 @@ class WTVShared {
return (this.isMiniBrowser(ssid_session) || parseInt(ssid_session.get("wtv-system-version")) < minBuild) ? true : false;
}
/**
* Gets the user configuration from the user_config.json file
* @returns {object} User configuration object
* @notice If the file does not exist, it will return an empty object
* @notice If the file exists but cannot be parsed, it will terminate the process with an error message
*/
getUserConfig() {
try {
var user_config_filename = this.getAbsolutePath("user_config.json", this.appdir);
@@ -577,7 +717,6 @@ class WTVShared {
return ssid_obj;
}
/**
* Alias for parseSSID, but just the manufacture info
* @param {string} ssid
@@ -589,6 +728,13 @@ class WTVShared {
else return this.parseSSID(ssid).manufacturer || null;
}
/**
* Reads the MiniSrv configuration files
* @param {boolean} user_config If true, also read user_config.json
* @param {boolean} notices If true, show notices
* @param {boolean} reload_notice If true, show reload notice
* @returns {object} The MiniSrv configuration object
*/
readMiniSrvConfig(user_config = true, notices = true, reload_notice = false) {
const log = (msg) => {
if (notices || reload_notice) console.log(msg);
@@ -642,8 +788,15 @@ class WTVShared {
log(" *** Configuration successfully read.");
this.minisrv_config = minisrv_config;
return this.minisrv_config;
}
}
/**
* Integrates the user configuration into the main configuration object
* @param {object} main The main configuration object
* @param {object} user The user configuration object
* @returns {object} The integrated configuration object
* @notice This will overwrite any existing keys in the main configuration with the user configuration
* */
integrateConfig(main, user) {
for (const key in user) {
if (typeof user[key] === 'object' && user[key] !== null && !Array.isArray(user[key])) {
@@ -655,7 +808,11 @@ class WTVShared {
return main;
}
/**
* Writes the user configuration to the user_config.json file
* @param {object} config Configuration object to write
* @returns {boolean} true if successful, false if not
*/
writeToUserConfig(config) {
if (config) {
try {
@@ -684,7 +841,6 @@ class WTVShared {
return false;
}
/**
* Generates a random string
* @param {int} len desired generated string length
@@ -718,7 +874,6 @@ class WTVShared {
return result;
}
/**
* Any alias of generateString with optional special characters enabled as well
* @param {string} len desired generated string length
@@ -737,6 +892,13 @@ class WTVShared {
return this.minisrv_config;
}
/**
* Wraps a string to a specified length, breaking at whitespace
* @param {string} string The string to wrap
* @param {number} len The maximum line length
* @param {string} join The string to join the wrapped lines with (default is "\n")
* @returns {string} The wrapped string
*/
lineWrap(string, len = 72, join = "\n") {
if (string.length <= len) return string;
@@ -771,6 +933,11 @@ class WTVShared {
return this.getFileLastModified(file).toUTCString();
}
/**
* Returns the Last-Modified date in a Date object with UTC time
* @param {string} file Path to a file
* @return {Date} Date object with UTC time
*/
getFileLastModifiedUTCObj(file) {
return new Date(new Date().setUTCSeconds(this.getFileLastModified(file).getUTCSeconds()));
}
@@ -842,6 +1009,11 @@ class WTVShared {
return decoded;
}
/**
* Censors the SSID by replacing parts of it with asterisks
* @param {string} ssid The SSID to censor
* @returns {string} Censored SSID
* */
censorSSID(ssid) {
if (ssid) {
if (ssid.slice(0, 8) === "MSTVSIMU") {
@@ -875,7 +1047,11 @@ class WTVShared {
return obj;
}
/**
* Filters sensitive information from request logs
* @param {object} obj The request log object to filter
* @returns {object} Filtered request log object
* */
filterRequestLog(obj) {
const passwordRegex = /(^pass$|passw(or)?d)/i;
var newobj = this.cloneObj(obj); // Clone the object once at the beginning
@@ -891,8 +1067,11 @@ class WTVShared {
return newobj;
}
/**
* Decodes post data from a request log object
* @param {object} obj The request log object
* @returns {object} The request log object with decoded post data
*/
decodePostData(obj) {
if (obj.post_data) {
const filterPasswords = this.minisrv_config.config.filter_passwords_in_logs === true;
@@ -924,11 +1103,12 @@ class WTVShared {
return obj;
}
// DON'T USE THIS
// Saved for reference until I come up with a better way
// If used, this will exceed the stack limit over time
unloadModule(moduleName) {
// Prevent usage
return;
// Search for the module in the require cache
let resolvedPath = require.resolve(moduleName);
@@ -973,8 +1153,6 @@ class WTVShared {
return this.path.resolve(path);
}
/**
* Returns a percentage
* @param {number} partialValue
@@ -1005,6 +1183,12 @@ class WTVShared {
return path.reverse().split(".")[0].reverse();
}
/**
* Gets a line from a file by line number
* @param {string} filename The file to read
* @param {number} lineNo The line number to read (0-indexed)
* @param {function} callback Callback function to call with the line or an error
* */
getLineFromFile(filename, lineNo, callback) {
let lineCount = 0;
const lineReader = this.readline.createInterface({
@@ -1034,8 +1218,6 @@ class WTVShared {
});
}
/**
* Checks if service is enabled or disabled in the config
* @param {string} service Service Name
@@ -1127,7 +1309,6 @@ class WTVShared {
return [headers, message];
}
/**
* Strips bad things from paths
* @param {string} base Base path
@@ -1169,7 +1350,6 @@ class WTVShared {
return pathModule.normalize(normalizedPath);
}
/**
* Makes sure an SSID is clean, and doesn't contain any exploitable characters
* @param {string} ssid
@@ -1181,12 +1361,22 @@ class WTVShared {
return ssid;
}
/**
* Makes sure a string path is clean, and doesn't contain any exploitable characters
* @param {string} path
* @returns {string|null} Sanitized Path
* */
makeSafeStringPath(path = "") {
path = path.replace(/[^\w]/g, "").replace(/\.\./g, "");
if (path.length == 0) path = null;
return path;
}
/**
* Unpacks a base64 compressed string
* @param {string|Buffer} data Base64 encoded compressed data
* @returns {string} Uncompressed string
* */
unpackCompressedB64(data) {
var data_buf = (typeof data === 'object') ? Buffer.from(data.toString('ascii'), 'base64') : Buffer.from(data, 'base64');
return this.zlib.inflateSync(data_buf, { finishFlush: this.zlib.Z_SYNC_FLUSH }).toString('ascii');
@@ -1252,6 +1442,7 @@ class WTVShared {
}
return keys.indexOf(key);
}
/**
* Moves an object to the desired location in the object (reorder)
* @param {string|int} currentKey Name of the object Key to move or the index to move
@@ -1322,7 +1513,7 @@ class clientShowAlert {
* @param {string} buttonlabel2 Button 2 Label
* @param {string} buttonaction2 Button 2 Action
* @param {string} noback If true, disables the back button
* @param {string} sound Sound to play
* @param {string} sound Sound to play when showing the alert (default is "none")
*/
constructor(image = null, message = null, buttonlabel1 = null, buttonaction1 = null, buttonlabel2 = null, buttonaction2 = null, noback = null, sound = null) {
this.message = message;

View File

@@ -9,14 +9,27 @@ class WTVShenanigans {
"DISABLE_HTML_SANITIZER": 5 // disables HTML Sanitizer, allowing all sorts of chaos in email/usenet posts and signatures
}
/**
* Creates an instance of WTVShenanigans.
* @param {Object} minisrv_config - The minisrv configuration object.
*/
constructor(minisrv_config) {
this.minisrv_config = minisrv_config;
}
/**
* Returns the current shenanigans level set in the minisrv configuration.
* @returns {boolean|number} The shenanigans level, or false if shenanigans are disabled.
*/
getShenanigansLevel() {
return this.minisrv_config.config.shenanigans;
}
/**
* Checks if a specific shenanigan is enabled based on the current shenanigans level.
* @param {number} value - The shenanigan level to check against.
* @returns {boolean} True if the shenanigan is enabled, false otherwise.
*/
checkShenanigan(value) {
var level = this.getShenanigansLevel();

View File

@@ -1,4 +1,5 @@
const LZSS = require("./LZSS.js");
const WTVShared = require("./WTVShared.js")['WTVShared'];
const WhitespaceInstruction = {
ADD_NONE: 0,
@@ -278,7 +279,7 @@ class WTVTellyScriptTokenizer {
this.tokenizeIdentifierOrConstant(checkSequence);
}
} else {
// Not alphanumeric <20> try symbol sequence.
// Not alphanumeric <20> try symbol sequence.
this.index = currentIdx;
checkSequence = this.buildCheckSequence(ch, "^[\\-+=<>!\\|\\&]$");
if (this.replacements.has(checkSequence)) {
@@ -1048,7 +1049,7 @@ class WTVTellyScriptMinifier {
class WTVTellyScript {
// --- TellyScript Class ---
/*
/**
* Constructs a new TellyScript object.
* @param {Uint8Array|string} data - The TellyScript data (either packed, tokenized, or raw).
* @param {number} dataState - One of TellyScriptState (default: PACKED).
@@ -1064,10 +1065,14 @@ class WTVTellyScript {
this.raw_data = null;
this.preprocessor_definitions = preprocessor_definitions;
this.version_minor = version_minor;
this.wtvshared = new WTVShared();
this.process(data, dataState);
}
/**
* Preprocesses the tellscript data based on the current preprocessor definitions.
* It handles directives like #ifdef, #ifndef, #if, #else, #endif.
*/
preprocess() {
var definitions = this.preprocessor_definitions || {};
// Split input into lines (handling CRLF and LF)
@@ -1140,6 +1145,10 @@ class WTVTellyScript {
}
/**
* Minifies the TellyScript data.
* @returns {string} The minified TellyScript data.
*/
minify() {
let minifier = new WTVTellyScriptMinifier();
this.raw_data = minifier.minify(this);
@@ -1148,51 +1157,18 @@ class WTVTellyScript {
this.pack();
}
ipToHex(ip) {
const parts = ip.split('.');
if (parts.length !== 4) {
throw new Error('Invalid IP address');
}
let num = 0;
for (let i = 0; i < 4; i++) {
const part = parseInt(parts[i], 10);
if (part < 0 || part > 255) {
throw new Error('Invalid IP address');
}
num = (num << 8) | part;
}
// Convert to unsigned 32-bit number before converting to hex
return "0x" + (num >>> 0).toString(16).toUpperCase();
}
setTemplateVars(service_name, dialin_number, DNS1IP, DNS2IP) {
this.raw_data = this.raw_data.replaceAll("%ServiceName%", service_name);
this.raw_data = this.raw_data.replaceAll("%DialinNumber%", dialin_number);
this.raw_data = this.raw_data.replaceAll("%DNSIP1%", DNS1IP);
this.raw_data = this.raw_data.replaceAll("%DNSIP2%", DNS2IP);
this.raw_data = this.raw_data.replaceAll("%DNS1%", this.ipToHex(DNS1IP));
this.raw_data = this.raw_data.replaceAll("%DNS2%", this.ipToHex(DNS2IP));
this.raw_data = this.raw_data.replaceAll("%DNS1%", this.wtvshared.ipToHex(DNS1IP));
this.raw_data = this.raw_data.replaceAll("%DNS2%", this.wtvshared.ipToHex(DNS2IP));
}
// --- Big Endian Converter Helpers ---
toUint32(bytes, offset) {
return (
(bytes[offset] << 24) >>> 0 |
(bytes[offset + 1] << 16) |
(bytes[offset + 2] << 8) |
(bytes[offset + 3])
) >>> 0;
}
uint32ToBytes(num) {
return [
(num >>> 24) & 0xff,
(num >>> 16) & 0xff,
(num >>> 8) & 0xff,
num & 0xff,
];
}
// --- CRC32 Calculation ---
@@ -1253,7 +1229,7 @@ class WTVTellyScript {
autoDetectState(data) {
if (data instanceof Uint8Array) {
if (data.length > 4) {
const magic = this.toUint32(data, 0);
const magic = this.wtvshared.toUint32(data, 0);
if (magic === 0x414e4459) { // "ANDY"
this.tellyscript_type = TellyScriptType.ORIGINAL;
return TellyScriptState.PACKED;
@@ -1312,19 +1288,23 @@ class WTVTellyScript {
}
// --- Unpacking ---
/**
* Unpacks the packed TellyScript data
* @returns {Uint8Array} The unpacked TellyScript data
*/
unpack() {
// Read header fields from the first 36 bytes.
const headerBytes = this.packed_data.slice(0, PACKED_TELLYSCRIPT_HEADER_SIZE);
this.packed_header = {
magic: String.fromCharCode(...headerBytes.slice(0, 4)),
version_major: this.toUint32(headerBytes, 4),
version_minor: this.toUint32(headerBytes, 8),
script_id: this.toUint32(headerBytes, 12),
script_mod: this.toUint32(headerBytes, 16),
compressed_data_length: this.toUint32(headerBytes, 20),
decompressed_data_length: this.toUint32(headerBytes, 24),
decompressed_checksum: this.toUint32(headerBytes, 28),
unknown1: this.toUint32(headerBytes, 32),
version_major: this.wtvshared.toUint32(headerBytes, 4),
version_minor: this.wtvshared.toUint32(headerBytes, 8),
script_id: this.wtvshared.toUint32(headerBytes, 12),
script_mod: this.wtvshared.toUint32(headerBytes, 16),
compressed_data_length: this.wtvshared.toUint32(headerBytes, 20),
decompressed_data_length: this.wtvshared.toUint32(headerBytes, 24),
decompressed_checksum: this.wtvshared.toUint32(headerBytes, 28),
unknown1: this.wtvshared.toUint32(headerBytes, 32),
};
// Extract compressed data from the remainder of the packed_data.
@@ -1337,21 +1317,30 @@ class WTVTellyScript {
return this.tokenized_data;
}
// --- Detokenization ---
/**
* Detokenizes the tokenized TellyScript data
* @returns {string} The detokenized TellyScript data
*/
detokenize() {
// Uses the previously defined TellyScriptDetokenizer class.
this.raw_data = new WTVTellyScriptDetokenizer(this.tokenized_data).detokenize();
return this.raw_data;
}
// --- Tokenization ---
/**
* Tokenizes the raw TellyScript data
* @returns {Uint8Array} The tokenized TellyScript data
*/
tokenize() {
// Uses the previously defined TellyScriptTokenizer class.
this.tokenized_data = new WTVTellyScriptTokenizer(this.raw_data).tokenize();
return this.tokenized_data;
}
// --- Packing ---
/**
* Packs the tokenized TellyScript data into a packed format
* @returns {Uint8Array} The packed TellyScript data
*/
pack() {
// Compress tokenized data using LZSS.
const comp = new LZSS();
@@ -1399,14 +1388,14 @@ class WTVTellyScript {
buffer[i] = header.magic.charCodeAt(i);
}
// Next fields: each 4 bytes in Big Endian order.
buffer.set(this.uint32ToBytes(header.version_major), 4);
buffer.set(this.uint32ToBytes(header.version_minor), 8);
buffer.set(this.uint32ToBytes(header.script_id), 12);
buffer.set(this.uint32ToBytes(header.script_mod), 16);
buffer.set(this.uint32ToBytes(header.compressed_data_length), 20);
buffer.set(this.uint32ToBytes(header.decompressed_data_length), 24);
buffer.set(this.uint32ToBytes(header.decompressed_checksum), 28);
buffer.set(this.uint32ToBytes(header.unknown1), 32);
buffer.set(this.wtvshared.uint32ToBytes(header.version_major), 4);
buffer.set(this.wtvshared.uint32ToBytes(header.version_minor), 8);
buffer.set(this.wtvshared.uint32ToBytes(header.script_id), 12);
buffer.set(this.wtvshared.uint32ToBytes(header.script_mod), 16);
buffer.set(this.wtvshared.uint32ToBytes(header.compressed_data_length), 20);
buffer.set(this.wtvshared.uint32ToBytes(header.decompressed_data_length), 24);
buffer.set(this.wtvshared.uint32ToBytes(header.decompressed_checksum), 28);
buffer.set(this.wtvshared.uint32ToBytes(header.unknown1), 32);
return buffer;
}