- numerous bug fixes
 - wtv-news goodies, ready for local testing
   - custom patched nntp-server node module with support for POSTing
   - should be able to post locally
   - 4 groups are made by default
   - can override in user_config.json (look at the config.json changes but dont do them there)
   - can sync down from an upstream server with sync_nntp.js
   - sync does not push new posts to upstream yet
This commit is contained in:
zefie
2022-10-11 21:44:18 -04:00
parent 1165b245ce
commit b89e0e932c
181 changed files with 4333 additions and 688 deletions

View File

@@ -40,20 +40,7 @@ class WTVClientSessionData {
this.ssid = ssid;
this.data_store = new Array();
this.session_store = {};
this.lockdownWhitelist = [
"wtv-1800:/preregister",
"wtv-head-waiter:/login",
"wtv-head-waiter:/ValidateLogin",
"wtv-head-waiter:/login-stage-two",
"wtv-head-waiter:/relogin",
"wtv-head-waiter:/ROMCache/Spacer.gif",
"wtv-head-waiter:/ROMCache/NameStrip.gif",
"wtv-head-waiter:/images/PasswordBanner.gif",
"wtv-head-waiter:/ROMCache/UtilityBullet.gif",
"wtv-head-waiter:/images/NameBanner.gif",
"wtv-head-waiter:/bad-disk",
"wtv-log:/log"
];
this.lockdownWhitelist = 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)
@@ -61,14 +48,16 @@ class WTVClientSessionData {
this.loginWhitelist = Object.assign([], this.lockdownWhitelist); // clone lockdown whitelist into login whitelist
this.loginWhitelist.push("wtv-head-waiter:/choose-user");
this.loginWhitelist.push("wtv-head-waiter:/password");
this.loginWhitelist.push("http://*"); // allow http proxy without login
this.loginWhitelist.push("https://*"); // allow https proxy without login
}
assignMailStore() {
this.mailstore = new WTVMail(this.minisrv_config, this)
}
assignFavoriteStore() {
this.mailstore = this.favstore = new WTVFavorites(this.minisrv_config, this)
}
createWTVSecSession() {
return new WTVSec(this.minisrv_config)
}
@@ -79,22 +68,39 @@ class WTVClientSessionData {
var total_unread_messages = 0;
for (var i = 0; i < this.minisrv_config.config.user_accounts.max_users_per_account; i++) {
var subUserSession = new this.constructor(this.minisrv_config, this.ssid);
subUserSession.switchUserID(i, false, false);
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;
}
switchUserID(user_id, update_mail = true, update_ticket = true) {
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 = user_id;
this.loadSessionData();
if (this.isRegistered()) this.assignMailStore();
if (this.data_store.wtvsec_login && update_ticket) this.setTicketData('user_id', 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) {
@@ -153,8 +159,14 @@ class WTVClientSessionData {
if (f.substr(0, 4) == "user") {
var user_file = master_directory + self.path.sep + f + self.path.sep + f + ".json";
if (self.fs.existsSync(user_file)) {
if (f == "user0") account_data['subscriber'] = JSON.parse(this.fs.readFileSync(user_file));
else account_data[f] = JSON.parse(this.fs.readFileSync(user_file));
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", ''))
}
}
}
}
@@ -426,10 +438,6 @@ class WTVClientSessionData {
return encoded_passwd.toString(CryptoJS.enc.Base64);
}
generatePassword(len) {
return CryptoJS.lib.WordArray.random(Math.round(len / 2)).toString(CryptoJS.enc.Hex);
}
setUserPassword(passwd) {
var encoded_passwd = this.encodePassword(passwd);
this.setSessionData("subscriber_password", encoded_passwd);
@@ -465,7 +473,6 @@ class WTVClientSessionData {
}
isUserLoggedIn() {
if (!this.getUserPasswordEnabled()) return true; // no password is set so always validate
var password_valid = this.get("password_valid");
return (password_valid);
}

View File

@@ -8,7 +8,7 @@ class WTVGuide {
constructor(minisrv_config, session_data, socket, runScriptInVM) {
if (!minisrv_config) throw ("minisrv_config required");
if (!session_data) throw ("WTVClientSessionData required");
var WTVShared = require("./WTVShared.js")['WTVShared'];
const WTVShared = require("./WTVShared.js")['WTVShared'];
this.minisrv_config = minisrv_config;
this.session_data = session_data;
this.wtvshared = new WTVShared(minisrv_config);
@@ -24,8 +24,8 @@ class WTVGuide {
switch (topic.toLowerCase()) {
case "glossary":
var template = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/templates/glossary.js");
var glossary_datafile = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/glossary.json");
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;
@@ -44,13 +44,9 @@ class WTVGuide {
var start = definition.indexOf(search) + search.length;
original_start = start;
// handle <word="whatever">
console.log("start:", start)
console.log("debug > position:", definition.substr(start, 1));
if (definition.substr(start, 1) != ">") {
console.log("debug: detecting word in tag")
start++; // +1 to skip =
end = definition.indexOf(">", start);
console.log("debug > position 2:", definition.indexOf(">", start));
link_word_override = definition.substring(start, end);
// strip any quotes
if (link_word_override.substr(0, 1).match(/[\"\']/)) link_word_override = link_word_override.substring(1);
@@ -60,10 +56,8 @@ class WTVGuide {
link_word_start_letter = link_word_for_link.substr(0, 1).toUpperCase();
start = end + 1; // update start pos for rest of processing
} else {
console.log("debug: generating word")
start++;
}
console.log("end:", end)
end = definition.indexOf("</word>", start);
var link_word = definition.substring(start, end);
if (!link_word_for_link) link_word_for_link = link_word.replace(/ /g, '').replace(/\'/g,'').replace(/\"/g,'').toLowerCase();
@@ -72,13 +66,6 @@ class WTVGuide {
var link = `wtv-guide:/help?topic=Glossary&subtopic=${link_word_start_letter}&page=${link_word_for_link}&word=${encodeURIComponent(link_word_override)}`
var new_definition = definition.substring(0, original_start - search.length) + `<a href="${link}">${link_word}</a>` + definition.substring(end + 7);
console.log("start:", start)
console.log("end:", end)
console.log("link_word:", link_word)
console.log("link_word_for_link:", link_word_for_link);
console.log("link_word_start_letter:", link_word_start_letter);
console.log("link:", link)
console.log("new_definition:", new_definition)
definition = new_definition;
}
// replaces <boxname> with the friendly name of the type of unit the user has
@@ -110,7 +97,7 @@ class WTVGuide {
}
} else {
// glossary letter word index
var template = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/templates/glossary_word_index.js");
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 = [];
@@ -133,9 +120,8 @@ class WTVGuide {
case "index":
switch (subtopic.toLowerCase()) {
case "glossary":
var template = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/templates/glossary_index.js");
console.log(template);
var glossary_datafile = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/glossary.json");
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;
@@ -154,8 +140,8 @@ class WTVGuide {
// fallback to old js file method
try {
var prerendered = null;
if (!page) prerendered = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/prerendered/" + topic + "/" + subtopic + ".js");
else prerendered = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + "/wtv-guide/prerendered/" + topic + "/" + subtopic + "/" + page + ".js");
if (!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;
@@ -184,7 +170,7 @@ class WTVGuide {
console.log(" * wtv-template error:", e)
}
// unload and clean up module
wtvshared.unloadModule(template);
this.wtvshared.unloadModule(template);
}
// return generated page

View File

@@ -39,7 +39,7 @@ class WTVMail {
this.trashMailboxName
];
this.defaultColors = {
bgcolor: "#171726",
bgcolor: "#191919",
text: "#82A9D9",
link: "#BDA73A",
vlink: "#62B362"
@@ -156,7 +156,7 @@ class WTVMail {
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) {
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);
@@ -177,7 +177,8 @@ class WTVMail {
"unread": true,
"attachments": attachments,
"url": url,
"url_title": url_title
"url_title": url_title,
"allow_html": allow_html
}
try {
if (this.fs.existsSync(message_file_out)) {
@@ -202,14 +203,53 @@ class WTVMail {
}
createWelcomeMessage() {
var from_addr = (this.minisrv_config.config.service_owner_account) ? this.minisrv_config.config.service_owner_account : this.minisrv_config.config.service_owner;
from_addr += "@" + this.minisrv_config.config.service_name;
var from_name = this.minisrv_config.config.service_owner
var 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 subj = "Welcome to " + this.minisrv_config.config.service_name;
var msg = "poop";
return this.createMessage(0, from_addr, to_addr, msg, subj, from_name, to_name, null, null, true);
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) {

View File

@@ -11,7 +11,7 @@ class WTVMime {
constructor(minisrv_config) {
var WTVShared = require("./WTVShared.js")['WTVShared'];
const { WTVShared } = require("./WTVShared.js");
this.minisrv_config = minisrv_config;
this.wtvshared = new WTVShared(minisrv_config);
if (!String.prototype.reverse) {
@@ -199,6 +199,67 @@ class WTVMime {
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;

View File

@@ -2,23 +2,61 @@ 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;
constructor(minisrv_config, service_name) {
this.minisrv_config = minisrv_config;
this.service_name = service_name;
this.client = new this.newsie({
host: this.minisrv_config.services[service_name].upstream_address,
port: this.minisrv_config.services[service_name].upstream_port
})
const { WTVShared } = require("./WTVShared.js");
this.wtvshared = new WTVShared(minisrv_config);
}
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) {
resolve(true);
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);
@@ -27,21 +65,63 @@ class WTVNews {
});
}
listGroup(group) {
listGroup(group, page = 0, limit = 100, raw_range = null) {
// list of articles from group
return new Promise((resolve, reject) => {
this.client.listGroup(group).then((data) => {
resolve(data);
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: listGroup", e);
reject(`No such group <b>${group}</b>`);
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) => {
console.log('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(true);
if (response.code == 211) resolve(response);
else reject(`No such group <b>${group}</b>`);
}).catch((e) => {
console.error(" * WTVNews Error:", "Command: selectGroup", e);
@@ -50,20 +130,102 @@ class WTVNews {
});
}
getArticle(articleID) {
getArticle(articleID, get_next_last = true) {
var articleID = parseInt(articleID);
return new Promise((resolve, reject) => {
var promises = [];
this.client.article(articleID).then((data) => {
resolve(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", e);
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}`);
@@ -72,6 +234,19 @@ class WTVNews {
});
}
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) => {
@@ -87,24 +262,263 @@ class WTVNews {
});
}
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.log('Error getting articleID',article, e)
reject(e)
});
}).catch((e) => {
console.log('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 failed = false;
var promises = [];
for (var article in NGArticles) {
if (failed) continue;
if (article == "getCaseInsensitiveKey") continue;
this.getHeader(NGArticles[article]).then((data) => {
if (data.article) messages.push(data.article)
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) => {
console.log(e, article);
failed = 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) => {
var section_header_match = line.match(/^Content\-/i)
if (section_header_match) {
var section_match = line.match(/^Content\-Type\: (.+)\;/i)
if (section_match) {
if (section_match[1].match("text/plain")) {
section_type = section_match[1].match("text/plain")[1];
message_type = section_type;
} else {
section_type = section_match[1];
attachments[i].content_type = section_match[1]
}
}
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 {
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, "date": article_date })
});
relations.sort((a, b) => { return (a.date - b.date) });
Object.keys(relations).forEach((j) => {
sorted.push(relations[j]);
});
}
if (!failed) resolve(messages);
else reject("Could not read message list", failed);
});
})
return sorted;
}
}
module.exports = WTVNews;

View File

@@ -0,0 +1,476 @@
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_path = this.wtvshared.getAbsolutePath(this.minisrv_config.config.ServiceDeps + '/wtv-news');
var tls_options = {
ca: this.fs.readFileSync(tls_path + this.path.sep + 'localserver_ca.pem'),
key: this.fs.readFileSync(tls_path + this.path.sep + 'localserver_key.pem'),
cert: this.fs.readFileSync(tls_path + this.path.sep + '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;

View File

@@ -24,7 +24,6 @@ class WTVRegister {
checkUsernameSanity(username) {
var regex_str = "^([A-Za-z0-9\-\_]{" + this.minisrv_config.config.user_accounts.min_username_length + "," + this.minisrv_config.config.user_accounts.max_username_length + "})$";
var regex = new RegExp(regex_str);
console.log(username, username.length, regex.test(username));
return regex.test(username);
}
@@ -37,12 +36,14 @@ class WTVRegister {
if (directory) search_dir = directory;
this.fs.readdirSync(search_dir).forEach(file => {
if (self.fs.lstatSync(search_dir + self.path.sep + file).isDirectory() && !return_val) {
return_val = self.checkUsernameAvailable(username, search_dir + self.path.sep + file);
if (search_dir.match(/minisrv\_internal\_nntp/)) return;
return_val = !self.checkUsernameAvailable(username, search_dir + self.path.sep + file);
}
if (!file.match(/.*\.json/ig)) return;
if (!file.match(/user.*\.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);
console.log(temp_session_data.subscriber_username.toLowerCase());
if (temp_session_data.subscriber_username.toLowerCase() == username.toLowerCase()) {
return_val = true;
}

View File

@@ -1,7 +1,7 @@
/**
* Shared functions across all classes and apps
*/
const CryptoJS = require('crypto-js');
class WTVShared {
@@ -9,11 +9,11 @@ class WTVShared {
fs = require('fs');
v8 = require('v8');
zlib = require('zlib');
CryptoJS = require('crypto-js');
html_entities = require('html-entities'); // used externally by service scripts
sanitizeHtml = require('sanitize-html');
iconv = require('iconv-lite');
parentDirectory = process.cwd()
extend = require('util')._extend;
minisrv_config = [];
@@ -40,6 +40,25 @@ class WTVShared {
}
}
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;
}
getServiceString(service, overrides = {}) {
// used externally by service scripts
if (service === "all") {
@@ -67,7 +86,7 @@ class WTVShared {
if (typeof val === 'string')
val = val.toLowerCase();
return val === true || val === "true" || val === 1;
return (val === true || val == "on" || val === "true" || val === 1);
}
@@ -123,6 +142,7 @@ class WTVShared {
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;
}
@@ -130,7 +150,37 @@ class WTVShared {
}
isHTML(str) {
return /<[a-z][\s\S]*>/i.test(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) {
@@ -197,6 +247,36 @@ class WTVShared {
}
}
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...");
@@ -294,7 +374,8 @@ class WTVShared {
var new_user_config = {};
Object.assign(new_user_config, minisrv_user_config, config);
if (this.minisrv_config.config.debug_flags.debug) console.log(" * Writing new user configuration...");
this.fs.writeFileSync(this.getAbsolutePath("user_config.json", parentDirectory), JSON.stringify(new_user_config, null, "\t"));
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) {
@@ -308,12 +389,48 @@ class WTVShared {
}
}
}
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
@@ -389,7 +506,7 @@ class WTVShared {
return obj.substr(0, 6) + ('*').repeat(9);
}
} else {
var newobj = Object.assign({}, obj);
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"];
@@ -411,18 +528,22 @@ class WTVShared {
filterRequestLog(obj) {
if (this.minisrv_config.config.filter_passwords_in_logs === true) {
if (obj.query) {
var newobj = Object.assign({}, obj);
if (obj.post_data) newobj.post_data = obj.post_data;
Object.keys(newobj.query).forEach(function (k) {
var key = k.toLowerCase();
switch (true) {
case /passw(or)?d/.test(key):
case /^pass$/.test(key):
newobj.query[key] = ('*').repeat(newobj.query[key].length);
break;
}
});
return newobj;
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;
@@ -435,7 +556,7 @@ class WTVShared {
var post_obj = {};
post_obj.query = [];
try {
var post_text = obj.post_data.toString(this.CryptoJS.enc.Utf8);
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++) {
@@ -454,7 +575,7 @@ class WTVShared {
post_text = post_text.substring(0, post_text.length - 1);
obj.post_data = post_text.hexEncode();
} catch (e) {
obj.post_data = obj.post_data.toString(this.CryptoJS.enc.Hex);
obj.post_data = obj.post_data.toString(CryptoJS.enc.Hex);
}
} else {
// simple, no filter
@@ -558,34 +679,36 @@ class WTVShared {
doErrorPage(code, data = null, details = null, pc_mode = false) {
var headers = null;
var minisrv_config = this.minisrv_config;
switch (code) {
case 401:
if (data === null) data = "Authorization Required.";
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 = "The publisher of that page has not authorized you to view it.";
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 = "The service could not find the requested page.";
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 = this.minisrv_config.config.service_name + " ran into a technical problem.";
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;
@@ -652,7 +775,23 @@ class WTVShared {
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 {

View File

@@ -6,7 +6,11 @@
"UserServiceVault",
"ServiceVault"
],
"ServiceDeps": "ServiceDeps",
"ServiceTemplates": [
"UserTemplates",
"ServiceDeps/templates"
],
"ServiceDeps": "ServiceDeps",
"SessionStore": "SessionStore",
"SharedROMCache": "SharedROMCache",
"enable_shared_romcache": true,
@@ -31,8 +35,12 @@
"show_detailed_splash": true,
"show_diskmap": false,
"unauthorized_url": "wtv-1800:/unauthorized?",
"enable_port_isolation": true,
"allow_guests": true,
"domain_name": "wtv.zefie.com",
"ssid_block_list": [
"minisrv_internal_nntp"
],
"user_accounts": {
"max_users_per_account": 6,
"min_username_length": 5,
@@ -43,7 +51,30 @@
"min_length": 5,
"max_length": 32,
"form_size": 16
}
},
"errorMessages": {
"400": "${service_name} ran into a technical problem. Please try again.",
"401": "Authorization Required.",
"403": "The publisher of that page has not authorized you to view it.",
"404": "The service could not find the requested page.",
"500": "${service_name} ran into a technical problem. Please try again."
},
"lockdownWhitelist": [
"wtv-1800:/preregister",
"wtv-head-waiter:/login",
"wtv-head-waiter:/ValidateLogin",
"wtv-head-waiter:/login-stage-two",
"wtv-head-waiter:/relogin",
"wtv-head-waiter:/ROMCache/Spacer.gif",
"wtv-head-waiter:/ROMCache/NameStrip.gif",
"wtv-head-waiter:/images/PasswordBanner.gif",
"wtv-head-waiter:/ROMCache/UtilityBullet.gif",
"wtv-head-waiter:/images/NameBanner.gif",
"wtv-head-waiter:/bad-disk",
"wtv-head-waiter:/images/signin_new_mail.gif",
"wtv-head-waiter:/images/signin_no_mail.gif",
"wtv-log:/log"
]
},
"services": {
"wtv-head-waiter": {
@@ -60,10 +91,34 @@
},
"wtv-news": {
"port": 1605,
"disabled": true,
"local_nntp_port": 57319,
"local_nntp_requires_auth": true,
"modules": [
"WTVNews"
]
],
"featuredGroups": [
{
"name": "WebTV",
"group": "webtv.users",
"description": "A moderated discussion with WebTV customers"
},
{
"name": "Hacking",
"group": "alt.discuss.webtv.hacking",
"description": "Not advertiser friendly"
},
{
"name": "minisrv",
"group": "minisrv.users",
"description": "The server behind it all"
},
{
"name": "MIDIs",
"group": "alt.discuss.midis",
"description": "Explore the sounds of Beatnik with your WebTV"
}
],
"groupMetaRefreshInterval": 86400
},
"wtv-register": {
"port": 1607,
@@ -122,11 +177,17 @@
},
"wtv-guide": {
"port": 1621,
"connections": 3
"connections": 3,
"modules": [
"WTVGuide"
]
},
"wtv-mail": {
"port": 1608,
"connections": 3
"connections": 3,
"modules": [
"WTVNews"
]
},
"wtv-passport": {
"port": 1654