Files
minisrv/zefie_wtvp_minisrv/includes/WTVShared.js
zefie c2a3081afd v0.9.36
- BREAKING CHANGE from v0.9.35 and eariler: Move accounts into subfolder of SessionStore
 - viewergen and viewer updates from @GraspYonOx
 - new homepage theme inspired by @GraspYonOx
 - connect setup also mostly by @GraspYonOx
 - numerous bug fixes
 - nntp-server-zefie is now on npmjs
 - implement proper nodejs debugging
   - will start to phase out using console.log for actual debugging
   - existing "debug" (verbose) messages will stay as is
   - future code debugging will use debug() to debug WIP, therefore not showing to most users if it is accidently left in
2022-11-29 08:29:31 -05:00

970 lines
38 KiB
JavaScript
Raw Blame History

/**
* Shared functions across all classes and apps
*/
const CryptoJS = require('crypto-js');
class WTVShared {
path = require('path');
fs = require('fs');
v8 = require('v8');
zlib = require('zlib');
html_entities = require('html-entities'); // used externally by service scripts
sanitizeHtml = require('sanitize-html');
iconv = require('iconv-lite');
parentDirectory = process.cwd()
extend = require('util')._extend;
debug = require('debug')('WTVShared')
minisrv_config = [];
constructor(minisrv_config, quiet = false) {
if (minisrv_config == null) this.minisrv_config = this.readMiniSrvConfig(true, !quiet);
else this.minisrv_config = minisrv_config;
if (!String.prototype.reverse) {
String.prototype.reverse = function () {
var splitString = this.split("");
var reverseArray = splitString.reverse();
var joinArray = reverseArray.join("");
return joinArray;
}
}
if (!String.prototype.hexEncode) {
String.prototype.hexEncode = function () {
var result = '';
for (var i = 0; i < this.length; i++) {
result += this.charCodeAt(i).toString(16);
}
return result;
}
}
}
atob(a) {
const CryptoJS = require('crypto-js');
const enc = CryptoJS.enc.Base64.parse(a);
return CryptoJS.enc.Utf8.stringify(enc)
}
btoa(b) {
const CryptoJS = require('crypto-js');
const enc = CryptoJS.enc.Utf8.parse(b); // encodedWord Array object
return CryptoJS.enc.Base64.stringify(enc);
}
cloneObj(src) {
if (src instanceof RegExp) {
return new RegExp(src);
} else if (src instanceof Date) {
return new Date(src.getTime());
} else if (typeof src === 'object' && src !== null) {
var clone = null;
if (Array.isArray(src)) clone = [];
else clone = {};
var self = this;
Object.keys(src).forEach((k )=> {
clone[k] = self.cloneObj(src[k]);
});
return clone;
}
return src;
}
isAdmin(wtvclient, service_name = "wtv-admin") {
var WTVAdmin = require("./WTVAdmin.js");
var wtva = new WTVAdmin(this.minisrv_config, wtvclient, service_name);
var result = wtva.isAuthorized(true);
wtva, WTVAdmin = null;
return result;
}
parseJSON(json) {
if (!json) return null;
if (typeof json !== 'string') json = json.toString();
// from https://github.com/getify/JSON.minify/blob/javascript/minify.json.js
var tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
in_string = false,
in_multiline_comment = false,
in_singleline_comment = false,
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc,
prevFrom
;
tokenizer.lastIndex = 0;
while (tmp = tokenizer.exec(json)) {
lc = RegExp.leftContext;
rc = RegExp.rightContext;
if (!in_multiline_comment && !in_singleline_comment) {
tmp2 = lc.substring(from);
if (!in_string) {
tmp2 = tmp2.replace(/(\n|\r|\s)+/g, "");
}
new_str[ns++] = tmp2;
}
prevFrom = from;
from = tokenizer.lastIndex;
// found a " character, and we're not currently in
// a comment? check for previous `\` escaping immediately
// leftward adjacent to this match
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
// perform look-behind escaping match, but
// limit left-context matching to only go back
// to the position of the last token match
//
// see: https://github.com/getify/JSON.minify/issues/64
tmp2 = lc.substring(prevFrom).match(/\\+$/);
// start of string with ", or unescaped " character found to end string?
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) {
in_string = !in_string;
}
from--; // include " character in next catch
rc = json.substring(from);
}
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
in_multiline_comment = true;
}
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
in_multiline_comment = false;
}
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
in_singleline_comment = true;
}
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
in_singleline_comment = false;
}
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
new_str[ns++] = tmp[0];
}
}
new_str[ns++] = rc;
return JSON.parse(new_str.join(""));
}
isConfiguredService(service) {
if (this.minisrv_config.services[service]) {
if (!this.minisrv_config.services[service].disabled) return true;
}
return false;
}
getServiceString(service, overrides = {}) {
// used externally by service scripts
if (service === "all") {
var out = "";
Object.keys(minisrv_config.services).forEach(function (k) {
if (overrides.exceptions) {
Object.keys(overrides.exceptions).forEach(function (j) {
if (k != overrides.exceptions[j]) out += minisrv_config.services[k].toString(overrides) + "\n";
});
} else {
out += minisrv_config.services[k].toString(overrides) + "\n";
}
});
return out;
} else {
if (!this.minisrv_config.services[service]) {
throw ("SERVICE ERROR: Attempted to provision unconfigured service: " + service)
} else {
return this.minisrv_config.services[service].toString(overrides);
}
}
}
parseBool(val) {
if (typeof val === 'string')
val = val.toLowerCase();
return (val === true || val == "on" || val === "true" || val === 1);
}
getQueryString(query) {
// for easy retrofitting old code to work with the webtvism of allowing multiple of the same query name
// pass it the query, and it will return a string regardless. if its a string it just sends it back
// if its an array we just pull the last object
if (typeof (query) === "object")
return query[(Object.keys(query).length - 1)];
else
return query
}
htmlEntitize(string, process_newline = false) {
string = this.html_entities.encode(string).replace(/&apos;/g, "'");
if (process_newline) string = string.replace(/\n/gi, "<br>").replace(/\r/gi, "");
return string;
}
sanitizeSignature(string) {
var allowedSchemes = ['http', 'https', 'ftp', 'mailto'];
var self = this;
Object.keys(this.minisrv_config.services).forEach(function (k) {
var flags = self.minisrv_config.services[k].flags;
if (flags) {
if (flags == "0x00000004" || flags == "0x00000007") {
allowedSchemes.push(self.minisrv_config.services[k].name);
}
}
});
var allowedProtocols = allowedSchemes;
// allow links to services flagged as "wideopen"
Object.keys(this.minisrv_config.services).forEach((k) => {
var flag = parseInt(this.minisrv_config.services[k].flags, 16);
if (flag === 4 || flag === 7) {
allowedProtocols.push(k);
}
});
const clean = this.sanitizeHtml(string, {
allowedTags: ['a', 'audioscope', 'b', 'bgsound', 'big', 'blackface', 'blockquote', 'bq', 'br', 'caption', 'center', 'cite', 'c', 'dd', 'dfn', 'div', 'dl', 'dt', 'fn', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'html', 'i', 'img', 'label', 'li', 'link', 'listing', 'em', 'marquee', 'nobr', 'note', 'ol', 'p', 'plaintext', 'pre', 's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'tbody', 'table', 'td', 'th', 'tr', 'tt', 'u', 'ul'],
disallowedTagsMode: 'discard',
allowedAttributes: {
a: ['href', 'name', 'target'],
img: ['src', 'alt', 'title', 'width', 'height', 'loading'],
bgsound: ['src', 'loop'],
font: ['size', 'name', 'color'],
marquee: ['speed'],
},
allowedSchemes: allowedSchemes,
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
exclusiveFilter: function (frame) {
var allowed = false;
Object.keys(frame.attribs).forEach((k) => {
if (k == "href" || k == "background" || k == "src") {
var value = frame.attribs[k];
Object.keys(allowedProtocols).forEach((j) => {
if (value.startsWith(allowedProtocols[j])) {
allowed = true;
}
})
}
});
console.log(frame, allowed);
return !allowed;
},
allowVulnerableTags: false,
allowProtocolRelative: false
}, true)
// todo: add missing user open tags (eg </i> if user did not close it) (might be done by sanitize-html?)
return clean;
}
isASCII(str) {
if (typeof str !== 'string') return false;
for (var i = 0, strLen = str.length; i < strLen; ++i) {
if (str.charCodeAt(i) > 127) return false;
}
return true;
}
isHTML(str) {
return /<\/?[a-z][\s\S]*>/i.test()
}
isBase64(str, opts) {
// from https://github.com/miguelmota/is-base64/blob/master/is-base64.js
if (str instanceof Boolean || typeof str === 'boolean') {
return false
}
if (!(opts instanceof Object)) {
opts = {}
}
if (opts.allowEmpty === false && str === '') {
return false
}
var regex = '(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\/]{3}=)?'
var mimeRegex = '(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)'
if (opts.mimeRequired === true) {
regex = mimeRegex + regex
} else if (opts.allowMime === true) {
regex = mimeRegex + '?' + regex
}
if (opts.paddingRequired === false) {
regex = '(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?'
}
return (new RegExp('^' + regex + '$', 'gi')).test(str)
}
utf8Decode(utf8String) {
if (typeof utf8String != 'string') throw new TypeError('parameter <20>utf8String<6E> is not a string');
// note: decode 3-byte chars first as decoded 2-byte strings could appear to be 3-byte char!
const unicodeString = utf8String.replace(
/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars
function (c) { // (note parentheses for precedence)
var cc = ((c.charCodeAt(0) & 0x0f) << 12) | ((c.charCodeAt(1) & 0x3f) << 6) | (c.charCodeAt(2) & 0x3f);
return String.fromCharCode(cc);
}
).replace(
/[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars
function (c) { // (note parentheses for precedence)
var cc = (c.charCodeAt(0) & 0x1f) << 6 | c.charCodeAt(1) & 0x3f;
return String.fromCharCode(cc);
}
);
return unicodeString;
}
decodeBufferText(buf) {
var out = "";
out = this.utf8Decode(this.iconv.decode(Buffer.from(buf),'ISO-8859-1'));
return out;
}
returnAbsolutePath(check_path) {
if (check_path.substring(0, 1) != this.path.sep && check_path.substring(1, 1) != ":") {
// non-absolute path, so use current directory as base
check_path = this.parentDirectory + this.path.sep + check_path;
} else {
// already absolute path
}
return check_path;
}
isMiniBrowser(ssid_session) {
return (ssid_session.get("wtv-need-upgrade") || ssid_session.get("wtv-used-8675309")) ? true : false;
}
isOldBuild(ssid_session) {
return (this.isMiniBrowser(ssid_session) || parseInt(ssid_session.get("wtv-system-version")) < 3500) ? true : false;
}
getUserConfig() {
try {
var user_config_filename = this.getAbsolutePath("user_config.json", this.parentDirectory);
if (this.fs.lstatSync(user_config_filename)) {
try {
var minisrv_user_config = this.parseJSON(this.fs.readFileSync(user_config_filename));
} catch (e) {
throw ("ERROR: Could not read user_config.json", e);
}
} else {
var minisrv_user_config = {}
}
return minisrv_user_config;
} catch (e) {
if (minisrv_config.config.debug_flags) {
if (minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration.");
}
}
}
moveObjectElement(currentKey, afterKey, obj, caseInsensitive = false) {
var result = {};
if (caseInsensitive) {
Object.keys(obj).forEach((k) => {
if (k.toLowerCase() == currentKey.toLowerCase()) {
currentKey = k;
return false;
}
})
}
var val = obj[currentKey];
delete obj[currentKey];
var next = -1;
var i = 0;
if (typeof afterKey == 'undefined' || afterKey == null) afterKey = '';
Object.keys(obj).forEach(function (k) {
var v = obj[k];
if ((afterKey == '' && i == 0) || next == 1) {
result[currentKey] = val;
next = 0;
}
if (k == afterKey || (caseInsensitive && k.toLowerCase() == afterKey.toLowerCase())) { next = 1; }
result[k] = v;
++i;
});
if (next == 1) {
result[currentKey] = val;
}
if (next !== -1) return result; else return obj;
}
readMiniSrvConfig(user_config = true, notices = true, reload_notice = false) {
if (notices || reload_notice) console.log(" *** Reading global configuration...");
try {
var minisrv_config = this.parseJSON(this.fs.readFileSync(this.getAbsolutePath("config.json", __dirname)));
} catch (e) {
throw ("ERROR: Could not read config.json", e);
}
var integrateConfig = function(main, user) {
Object.keys(user).forEach(function (k) {
if (typeof (user[k]) == 'object' && user[k] != null) {
// new entry
if (!main[k]) main[k] = new Array();
// go down the rabbit hole
main[k] = integrateConfig(main[k], user[k]);
} else {
// update main config
main[k] = user[k];
}
});
return main;
}
if (user_config) {
try {
if (notices || reload_notice) console.log(" *** Reading user configuration...");
var minisrv_user_config = this.getUserConfig()
if (!minisrv_user_config) throw "ERROR: Could not read user_config.json";
try {
minisrv_config = integrateConfig(minisrv_config, minisrv_user_config)
} catch (e) {
console.error("ERROR: Could not read user_config.json", e);
}
} catch (e) {
if (minisrv_config.config.debug_flags) {
if (minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration.");
}
}
}
// defaults
minisrv_config.config.debug_flags = [];
minisrv_config.config.debug_flags.debug = false;
minisrv_config.config.debug_flags.quiet = true; // will squash minisrv_config.config.debug_flags.debug even if its true
minisrv_config.config.debug_flags.show_headers = false;
if (minisrv_config.config.verbosity) {
switch (minisrv_config.config.verbosity) {
case 0:
minisrv_config.config.debug_flags.debug = false;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = false;
if (notices) console.log(" * Console Verbosity level 0 (quietest)")
break;
case 1:
minisrv_config.config.debug_flags.debug = false;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = true;
if (notices) console.log(" * Console Verbosity level 1 (headers shown)")
break;
case 2:
minisrv_config.config.debug_flags.debug = true;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = false;
if (notices) console.log(" * Console Verbosity level 2 (verbose without headers)")
break;
case 3:
minisrv_config.config.debug_flags.debug = true;
minisrv_config.config.debug_flags.quiet = true;
minisrv_config.config.debug_flags.show_headers = true;
if (notices) console.log(" * Console Verbosity level 3 (verbose with headers)")
break;
default:
minisrv_config.config.debug_flags.debug = true;
minisrv_config.config.debug_flags.quiet = false;
minisrv_config.config.debug_flags.show_headers = true;
if (notices) console.log(" * Console Verbosity level 4 (debug verbosity)")
break;
}
}
if (notices || reload_notice) console.log(" *** Configuration successfully read.");
this.minisrv_config = minisrv_config;
return this.minisrv_config;
}
writeToUserConfig(config) {
if (config) {
try {
var minisrv_user_config = this.getUserConfig();
// write back
try {
var new_user_config = {};
Object.assign(new_user_config, minisrv_user_config, config);
if (this.minisrv_config.config.debug_flags.debug) console.log(" * Writing new user configuration...");
this.fs.writeFileSync(this.getAbsolutePath("user_config.json", this.parentDirectory), JSON.stringify(new_user_config, null, "\t"));
return true;
}
catch (e) {
if (this.minisrv_config.config.debug_flags) {
if (this.minisrv_config.config.debug_flags.debug) console.error(" * WARNING: Could not update user config. Data may have been lost.", e);
}
}
} catch (e) {
if (this.minisrv_config.config.debug_flags) {
if (this.minisrv_config.config.debug_flags.debug) console.error(" * Notice: Could not find user configuration (user_config.json). Using default configuration.");
}
}
}
return false;
}
generateString(len, extra_chars = null) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
if (extra_chars) characters += extra_chars;
var charactersLength = characters.length;
for (var i = 0; i < len; i++) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
}
generatePassword(len, simple = false) {
return this.generateString(len, (simple) ? null : '!@#$%&()[]-_+=?.');
}
getMiniSrvConfig() {
return this.minisrv_config;
}
lineWrap(string, len = 72, join = "\n") {
if (string.length <= len) return string;
var split;
if (string.match(" ")) {
// split if text with space, respecting words
split = string.match(new RegExp('([\\s\\S]){1,' + len + '}?!\\S', "g"));
}
if (!split) {
// fallback if above failed, or if its just a really long word (eg base64)
split = string.match(new RegExp('.{1,' + len + '}', "g"));
} else Object.keys(split).forEach((k) => {
if (split[k].substr(0, 1) == ' ') split[k] = split[k].trim(' ');
});
if (split) return split.join(join);
else return null;
}
/**
* Returns the Last-Modified date in Unix Timestamp format
* @param {string} file Path to a file
*/
getFileLastModified(file) {
var stats = this.fs.lstatSync(file);
if (stats) return new Date(stats.mtimeMs);
return false;
}
/**
* Returns the Last-Modified date in a RFC7231 compliant UTC Date String
* @param {string} file Path to a file
*/
getFileLastModifiedUTCString(file) {
return this.getFileLastModified(file).toUTCString();
}
/**
* Returns a RFC7231 compliant UTC Date String from the current time
* @param {Number} offset Offset from current time (+/-)
* @returns {string} A RFC7231 compliant UTC Date String from the current time
*/
getUTCTime(offset = 0) {
return new Date((new Date).getTime() + offset).toUTCString();
}
urlEncodeBytes = (buf) => {
let encoded = ''
for (let i = 0; i < buf.length; i++) {
const charBuf = Buffer.from('00', 'hex')
charBuf.writeUInt8(buf[i])
const char = charBuf.toString()
// if the character is safe, then just print it, otherwise encode
if (isUrlSafe(char)) {
encoded += char
} else {
encoded += `%${charBuf.toString('hex').toUpperCase()}`
}
}
return encoded
}
urlDecodeBytes = (encoded) => {
let decoded = Buffer.from('')
for (let i = 0; i < encoded.length; i++) {
if (encoded[i] === '%') {
const charBuf = Buffer.from(`${encoded[i + 1]}${encoded[i + 2]}`, 'hex')
decoded = Buffer.concat([decoded, charBuf])
i += 2
} else {
const charBuf = Buffer.from(encoded[i])
decoded = Buffer.concat([decoded, charBuf])
}
}
return decoded
}
/**
* Returns a censored SSID
* @param {string|Array} obj SSID String or Headers Object
*/
filterSSID(obj) {
if (this.minisrv_config.config.hide_ssid_in_logs === true) {
if (typeof (obj) == "string") {
if (obj.substr(0, 8) == "MSTVSIMU") {
return obj.substr(0, 10) + ('*').repeat(10) + obj.substr(20);
} else if (obj.substr(0, 5) == "1SEGA") {
return obj.substr(0, 6) + ('*').repeat(6) + obj.substr(13);
} else {
return obj.substr(0, 6) + ('*').repeat(9);
}
} else {
var newobj = this.cloneObj(obj);
if (obj.post_data) newobj.post_data = obj.post_data;
if (newobj["wtv-client-serial-number"]) {
var ssid = newobj["wtv-client-serial-number"];
if (ssid.substr(0, 8) == "MSTVSIMU") {
newobj["wtv-client-serial-number"] = ssid.substr(0, 10) + ('*').repeat(10) + ssid.substr(20);
} else if (ssid.substr(0, 5) == "1SEGA") {
newobj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(6) + ssid.substr(13);
} else {
newobj["wtv-client-serial-number"] = ssid.substr(0, 6) + ('*').repeat(9);
}
}
return newobj;
}
} else {
return obj;
}
}
filterRequestLog(obj) {
if (this.minisrv_config.config.filter_passwords_in_logs === true) {
if (obj.query) {
var newobj = this.cloneObj(obj);
try {
Object.keys(obj.query).forEach(function (k) {
var key = k.toLowerCase();
switch (true) {
case /passw(or)?d/.test(key):
case /^pass$/.test(key):
newobj.query[key] = ('*').repeat(newobj.query[key].length);
break;
}
});
return newobj;
} catch (e) {
if (!this.minisrv_config.config.debug_flags.quiet) console.error(' *** error filtering logs', e);
return obj;
}
}
}
return obj;
}
decodePostData(obj) {
if (obj.post_data) {
if (this.minisrv_config.config.filter_passwords_in_logs === true) {
// complex, to filter
var post_obj = {};
post_obj.query = [];
try {
var post_text = obj.post_data.toString(CryptoJS.enc.Utf8);
if (post_text.length > 0) {
post_text = post_text.split("&");
for (let i = 0; i < post_text.length; i++) {
var qraw_split = post_text[i].split("=");
if (qraw_split.length == 2) {
var k = qraw_split[0];
post_obj.query[k] = unescape(post_text[i].split("=")[1].replace(/\+/g, "%20"));
}
}
}
var post_obj = this.filterRequestLog(post_obj);
post_text = "";
Object.keys(post_obj.query).forEach(function (k) {
post_text += k + "=" + post_obj.query[k] + "&";
});
post_text = post_text.substring(0, post_text.length - 1);
obj.post_data = post_text.hexEncode();
} catch (e) {
obj.post_data = obj.post_data.toString(CryptoJS.enc.Hex);
}
} else {
// simple, no filter
obj.post_data = obj.post_data.toString();
}
}
return obj;
}
unloadModule(moduleName) {
// for handling template classes
var solvedName = require.resolve(moduleName),
nodeModule = require.cache[solvedName];
if (nodeModule) {
for (var i = 0; i < nodeModule.children.length; i++) {
var child = nodeModule.children[i];
this.unloadModule(child.filename);
}
delete require.cache[solvedName];
}
}
/**
* Returns an absolute path
* @param {string} path
* @param {string} directory Root directory
*/
getAbsolutePath(path, directory = null) {
if (directory) {
if (path.indexOf(directory) == -1) {
directory = this.getAbsolutePath(directory);
try {
if (this.fs.lstatSync(directory).isDirectory()) directory = directory + this.path.sep;
} catch (e) { }
path = directory + path;
}
}
return this.fixPathSlashes(this.path.resolve(path));
}
/**
* Returns a percentage
* @param {number} partialValue
* @param {number} totalValue
* @returns {number} percentage
*/
getPercentage = function (partialValue, totalValue) {
return Math.floor((100 * partialValue) / totalValue);
}
/**
* If the file ends with .gz, remove it
* @param {string} path
* @return {string} path without gz, or unmodified path if it isnt a gz
*/
getFilePath(path) {
var path_split = path.split('/');
path_split.pop();
return path_split.join('/');
}
/**
* Gets the file extension from a path
* @param {string} path
* @returns {String} File Extension (without dot)
*/
getFileExt(path) {
return path.reverse().split(".")[0].reverse();
}
getLineFromFile(filename, line_no, callback) {
var stream = this.fs.createReadStream(filename, {
flags: 'r',
encoding: 'utf-8',
fd: null,
bufferSize: 64 * 1024
});
var fileData = '';
stream.on('data', function (data) {
fileData += data;
// The next lines should be improved
var lines = fileData.split("\n");
if (lines.length >= +line_no) {
stream.destroy();
callback(null, lines[+line_no]);
}
});
stream.on('error', function () {
callback('Error', null);
});
stream.on('end', function () {
callback('File end reached without finding line', null);
});
}
doErrorPage(code, data = null, details = null, pc_mode = false, wtv_reset = false) {
var headers = null;
var minisrv_config = this.minisrv_config;
switch (code) {
case 401:
if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] });
if (pc_mode) headers = "401 Unauthorized\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
case 403:
if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] });
if (pc_mode) headers = "403 Forbidden\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
case 404:
if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] });
if (pc_mode) headers = "404 Not Found\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
case 400:
case 500:
if (data === null) data = minisrv_config.config.errorMessages[code].replace(/\$\{(\w{1,})\}/g, function (x) { return minisrv_config.config[x.replace("${", '').replace('}', '')] });
if (details) data += "<br>Details:<br>" + details;
if (pc_mode) headers = "500 Internal Server Error\n";
else headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
default:
if (data === null && this.minisrv_config.config.errorMessages[code]) data = minisrv_config.config.errorMessages[code].replace(/\$\{(.+)\}/g, function (x) { return minisrv_config.config[x.replace("${",'').replace('}','')] });
headers = code + " " + data + "\n";
headers += "Content-Type: text/html\n";
break;
}
if (wtv_reset && !pc_mode) {
headers += "wtv-service: reset\n";
headers += this.getServiceString('wtv-1800') + "\n";
headers += "wtv-visit: wtv-1800:/preregister?scriptless-visit-reason=999\n";
console.error(" * doErrorPage Called (sent wtv-reset):", code, data);
} else console.error(" * doErrorPage Called:", code, data);
return new Array(headers, data);
}
/**
* Strips bad things from paths
* @param {string} base Base path
* @param {string} target Sub path
*/
makeSafePath(base, target = null, force_forward_slash = false) {
if (target) {
target.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, "");
var targetPath = this.path.posix.normalize(target)
var output = this.fixPathSlashes(base + this.path.sep + targetPath);
} else {
base.replace(/[\|\&\;\$\%\@\"\<\>\+\,\\]/g, "");
var targetPath = this.path.posix.normalize(base)
var output = this.fixPathSlashes(targetPath);
}
return (force_forward_slash) ? output.replace(this.path.sep, '/') : output;
}
makeSafeUsername(username) {
return username.replace(/^([A-Za-z0-9\-\_]{5,16})$/, '');
}
/**
* Corrects any / or \ differences, if any for file paths
* @param {string} path
* @returns {string} corrected path
*/
fixPathSlashes(path) {
// fix slashes
if (this.path.sep == '/' && path.indexOf("\\") != -1) path = path.replace(/\\/g, this.path.sep);
else if (this.path.sep == "\\" && path.indexOf("/") != -1) path = path.replace(/\//g, this.path.sep);
// remove double slashes
while (path.indexOf(this.path.sep + this.path.sep) != -1) path = path.replace(this.path.sep + this.path.sep, this.path.sep);
return path;
}
/**
* Makes sure an SSID is clean, and doesn't contain any exploitable characters
* @param {string} ssid
* @returns {string} Sanitized SSID
*/
makeSafeSSID(ssid = "") {
ssid = ssid.replace(/[^a-zA-Z0-9]/g, "");
if (ssid.length == 0) ssid = null;
return ssid;
}
makeSafeStringPath(path = "") {
path = path.replace(/[^\w]/g, "").replace(/\.\./g, "");
if (path.length == 0) path = null;
return path;
}
unpackCompressedB64(data) {
var data_buf = (typeof data === 'object') ? Buffer.from(data.toString('ascii'), 'base64') : Buffer.from(data, 'base64');
return this.zlib.inflateSync(data_buf, { finishFlush: this.zlib.Z_SYNC_FLUSH }).toString('ascii');
}
packCompressedB64(data) {
return this.zlib.deflateSync(data, { 'level': 9 }).toString('base64');
}
getTemplate(service_name, path, path_only = false) {
var self = this;
var outdata = null;
var found = false
this.minisrv_config.config.ServiceTemplates.forEach(function (template_vault_dir) {
if (found) return;
var search = self.getAbsolutePath(template_vault_dir + self.path.sep + service_name + self.path.sep + path);
if (self.fs.existsSync(search)) {
if (path_only) outdata = search;
else outdata = self.fs.readFileSync(search);
if (!self.minisrv_config.config.debug_flags.quiet) console.log(" * Found " + search + " to handle template");
found = true;
return false;
}
});
return outdata;
}
}
class clientShowAlert {
message = null;
buttonlabel1 = null;
buttonlabel2 = null;
buttonaction1 = null;
buttonaction2 = null;
noback = null;
image = null;
constructor(image = null, message = null, buttonlabel1 = null, buttonaction1 = null, buttonlabel2 = null, buttonaction2 = null, noback = null, sound = null) {
this.message = message;
this.buttonlabel1 = buttonlabel1;
this.buttonlabel2 = buttonlabel2;
this.buttonaction1 = buttonaction1;
this.buttonaction2 = buttonaction2;
this.message = message;
this.noback = noback;
this.sound = sound;
if (this.sound === false) this.sound = "none";
if (typeof image === 'object') {
this.image = null;
Object.keys(image).forEach(function (k) {
if (this[k] === null) this[k] = image[k];
}, this);
} else {
this.image = image;
}
}
getURL() {
var url = "client:ShowAlert?";
if (this.message) url += "message=" + escape(this.message) + "&";
if (this.buttonlabel1) url += "buttonlabel1=" + escape(this.buttonlabel1) + "&";
if (this.buttonaction1) url += "buttonaction1=" + escape(this.buttonaction1) + "&";
if (this.buttonlabel2) url += "buttonlabel2=" + escape(this.buttonlabel2) + "&";
if (this.buttonaction2) url += "buttonaction2=" + escape(this.buttonaction2) + "&";
if (this.image) url += "image=" + escape(this.image) + "&";
if (this.sound) url += "sound=" + escape(this.sound) + "&";
if (this.noback) url += "noback=true&";
return url.substring(0, url.length - 1);
}
}
module.exports.WTVShared = WTVShared;
module.exports.clientShowAlert = clientShowAlert;