From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/test/fakeserver/Nntpd.jsm | 631 ++++++++++++++++++++++++++++++++ 1 file changed, 631 insertions(+) create mode 100644 comm/mailnews/test/fakeserver/Nntpd.jsm (limited to 'comm/mailnews/test/fakeserver/Nntpd.jsm') diff --git a/comm/mailnews/test/fakeserver/Nntpd.jsm b/comm/mailnews/test/fakeserver/Nntpd.jsm new file mode 100644 index 0000000000..f6d1be0a48 --- /dev/null +++ b/comm/mailnews/test/fakeserver/Nntpd.jsm @@ -0,0 +1,631 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file implements test NNTP servers + +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +var EXPORTED_SYMBOLS = [ + "NntpDaemon", + "NewsArticle", + "NNTP_POSTABLE", + "NNTP_REAL_LENGTH", + "NNTP_RFC977_handler", + "NNTP_RFC2980_handler", + "NNTP_RFC3977_handler", + "NNTP_Giganews_handler", + "NNTP_RFC4643_extension", +]; + +class NntpDaemon { + constructor(flags) { + this._groups = {}; + this._messages = {}; + this._flags = flags; + } + addGroup(group, postable) { + var flags = 0; + if (postable) { + flags |= NNTP_POSTABLE; + } + this._groups[group] = { keys: [], flags, nextKey: 1 }; + } + addArticle(article) { + this._messages[article.messageID] = article; + for (let group of article.groups) { + if (group in this._groups) { + var key = this._groups[group].nextKey++; + this._groups[group][key] = article; + this._groups[group].keys.push(key); + } + } + } + addArticleToGroup(article, group, key) { + this._groups[group][key] = article; + this._messages[article.messageID] = article; + this._groups[group].keys.push(key); + if (this._groups[group].nextKey <= key) { + this._groups[group].nextKey = key + 1; + } + } + removeArticleFromGroup(groupName, key) { + let group = this._groups[groupName]; + delete group[key]; + group.keys = group.keys.filter(x => x != key); + } + getGroup(group) { + if (this._groups.hasOwnProperty(group)) { + return this._groups[group]; + } + return null; + } + getGroupStats(group) { + if (group.keys.length == 0) { + return [0, 0, 0]; + } + var min = 1 << 30; + var max = 0; + group.keys.forEach(function (key) { + if (key < min) { + min = key; + } + if (key > max) { + max = key; + } + }); + + var length; + if (hasFlag(this._flags, NNTP_REAL_LENGTH)) { + length = group.keys.length; + } else { + length = max - min + 1; + } + + return [length, min, max]; + } + getArticle(msgid) { + if (msgid in this._messages) { + return this._messages[msgid]; + } + return null; + } +} + +function NewsArticle(text) { + this.headers = new Map(); + this.body = ""; + this.messageID = ""; + this.fullText = text; + + var headerMap; + [headerMap, this.body] = MimeParser.extractHeadersAndBody(text); + for (var [header, values] of headerMap._rawHeaders) { + var value = values[0]; + this.headers.set(header, value); + if (header == "message-id") { + var start = value.indexOf("<"); + var end = value.indexOf(">", start); + this.messageID = value.substring(start, end + 1); + } else if (header == "newsgroups") { + this.groups = value.split(/[ \t]*,[ \t]*/); + } + } + + // Add in non-existent fields + if (!this.headers.has("lines")) { + let lines = this.body.split("\n").length; + this.headers.set("lines", lines); + } +} + +/** + * This function converts an NNTP wildmat into a regular expression. + * + * I don't know how accurate it is wrt i18n characters, but its primary usage + * right now is just XPAT, where i18n effects are utterly unspecified, so I am + * not too concerned. + * + * This also neglects cases where special characters are in [] blocks. + */ +function wildmat2regex(wildmat) { + // Special characters in regex that aren't special in wildmat + wildmat = wildmat.replace(/[$+.()|{}^]/, function (str) { + return "\\" + str; + }); + wildmat = wildmat.replace(/(\\*)([*?])/, function (str, p1, p2) { + // TODO: This function appears to be wrong on closer inspection. + if (p1.length % 2 == 0) { + return p2 == "*" ? ".*" : "."; + } + return str; + }); + return new RegExp(wildmat); +} + +// NNTP FLAGS +var NNTP_POSTABLE = 0x0001; + +var NNTP_REAL_LENGTH = 0x0100; + +function hasFlag(flags, flag) { + return (flags & flag) == flag; +} + +// NNTP TEST SERVERS +// ----------------- +// To be comprehensive about testing and fallback, we define these varying +// levels of RFC-compliance: +// * RFC 977 solely (there's not a lot there!) +// * RFC 977 + 2980 (note that there are varying levels of this impl) +// * RFC 3977 bare bones +// * RFC 3977 full +// * RFC 3977 + post-3977 extensions +// * Giganews (Common newsserver for ISP stuff; highest importance) +// * INN 2.4 (Gold standard common implementation; second highest importance) +// Note too that we want various levels of brokenness: +// * Perm errors that require login +// * "I can't handle that" (e.g., news.mozilla.org only supports XOVER for +// searching with XHDR) +// * Naive group counts, missing articles +// * Limitations on what can be posted + +// This handler implements the bare minimum required by RFC 977. Actually, not +// even that much: IHAVE and SLAVE are not implemented, as those two are +// explicitly server implementations. +class NNTP_RFC977_handler { + constructor(daemon) { + this._daemon = daemon; + this.closing = false; + this.resetTest(); + } + resetTest() { + this.extraCommands = ""; + this.articleKey = null; + this.group = null; + } + ARTICLE(args) { + var info = this._selectArticle(args, 220); + if (info[0] == null) { + return info[1]; + } + + var response = info[1] + "\n"; + response += info[0].fullText.replace(/^\./gm, ".."); + response += "."; + return response; + } + BODY(args) { + var info = this._selectArticle(args, 222); + if (info[0] == null) { + return info[1]; + } + + var response = info[1] + "\n"; + response += info[0].body.replace(/^\./gm, ".."); + response += "."; + return response; + } + GROUP(args) { + var group = this._daemon.getGroup(args); + if (group == null) { + return "411 no such news group"; + } + + this.group = group; + this.articleKey = 0 in this.group.keys ? this.group.keys[0] : null; + + var stats = this._daemon.getGroupStats(group); + return ( + "211 " + + stats[0] + + " " + + stats[1] + + " " + + stats[2] + + " " + + args + + " group selected" + ); + } + HEAD(args) { + var info = this._selectArticle(args, 221); + if (info[0] == null) { + return info[1]; + } + + var response = info[1] + "\n"; + for (let [header, value] of info[0].headers) { + response += header + ": " + value + "\n"; + } + response += "."; + return response; + } + HELP(args) { + var response = "100 Why certainly, here is my help:\n"; + response += "Mozilla fake NNTP RFC 977 testing server"; + response += "Commands supported:\n"; + response += "\tARTICLE | [nnn]\n"; + response += "\tBODY\n"; + response += "\tGROUP group\n"; + response += "\tHEAD\n"; + response += "\tHELP\n"; + response += "\tLAST\n"; + response += "\tLIST\n"; + response += "\tNEWGROUPS\n"; + response += "\tNEWNEWS\n"; + response += "\tNEXT\n"; + response += "\tPOST\n"; + response += "\tQUIT\n"; + response += "\tSTAT\n"; + response += this.extraCommands; + response += "."; + return response; + } + LAST(args) { + if (this.group == null) { + return "412 no newsgroup selected"; + } + if (this.articleKey == null) { + return "420 no current article has been selected"; + } + return "502 Command not implemented"; + } + LIST(args) { + var response = "215 list of newsgroup follows\n"; + for (let groupname in this._daemon._groups) { + let group = this._daemon._groups[groupname]; + let stats = this._daemon.getGroupStats(group); + response += + groupname + + " " + + stats[1] + + " " + + stats[0] + + " " + + (hasFlag(group.flags, NNTP_POSTABLE) ? "y" : "n") + + "\n"; + } + response += "."; + return response; + } + NEWGROUPS(args) { + return "502 Command not implemented"; + } + NEWNEWS(args) { + return "502 Command not implemented"; + } + NEXT(args) { + if (this.group == null) { + return "412 no newsgroup selected"; + } + if (this.articleKey == null) { + return "420 no current article has been selected"; + } + return "502 Command not implemented"; + } + POST(args) { + this.posting = true; + this.post = ""; + return "340 Please continue"; + } + QUIT(args) { + this.closing = true; + return "205 closing connection - goodbye!"; + } + STAT(args) { + var info = this._selectArticle(args, 223); + return info[1]; + } + LISTGROUP(args) { + // Yes, I know this isn't RFC 977, but I doubt that mailnews will ever drop + // its requirement for this, so I'll stuff it in here anyways... + var group = args == "" ? this.group : this._daemon.getGroup(args); + if (group == null) { + return "411 This newsgroup does not exist"; + } + + var response = "211 Articles follow:\n"; + for (let key of group.keys) { + response += key + "\n"; + } + response += ".\n"; + return response; + } + + onError(command, args) { + return "500 command not recognized"; + } + onServerFault(e) { + return "500 internal server error: " + e; + } + onStartup() { + this.closing = false; + this.group = null; + this.article = null; + this.posting = false; + return "200 posting allowed"; + } + onMultiline(line) { + if (line == ".") { + if (this.posting) { + var article = new NewsArticle(this.post); + this._daemon.addArticle(article); + this.posting = false; + return "240 Wonderful article, your style is gorgeous!"; + } + } + + if (this.posting) { + if (line.startsWith(".")) { + line = line.substring(1); + } + + this.post += line + "\n"; + } + + return undefined; + } + postCommand(reader) { + if (this.closing) { + reader.closeSocket(); + } + reader.setMultiline(this.posting); + } + + /** + * Selects an article based on args. + * + * Returns an array of objects consisting of: + * # The selected article (or null if non was selected + * # The first line response + */ + _selectArticle(args, responseCode) { + var art, key; + if (args == "") { + if (this.group == null) { + return [null, "412 no newsgroup has been selected"]; + } + if (this.articleKey == null) { + return [null, "420 no current article has been selected"]; + } + + art = this.group[this.articleKey]; + key = this.articleKey; + } else if (args.startsWith("<")) { + art = this._daemon.getArticle(args); + key = 0; + + if (art == null) { + return [null, "430 no such article found"]; + } + } else { + if (this.group == null) { + return [null, "412 no newsgroup has been selected"]; + } + + key = parseInt(args); + if (key in this.group) { + this.articleKey = key; + art = this.group[key]; + } else { + return [null, "423 no such article number in this group"]; + } + } + + var respCode = + responseCode + " " + key + " " + art.messageID + " article selected"; + return [art, respCode]; + } +} + +class NNTP_RFC2980_handler extends NNTP_RFC977_handler { + DATE(args) { + return "502 Command not implemented"; + } + LIST(args) { + var index = args.indexOf(" "); + var command = index == -1 ? args : args.substring(0, index); + args = index == -1 ? "" : args.substring(index + 1); + command = command.toUpperCase(); + if ("LIST_" + command in this) { + return this["LIST_" + command](args); + } + return super.LIST(command + " " + args); + } + LIST_ACTIVE(args) { + return super.LIST(args); + } + MODE(args) { + if (args == "READER") { + return this.onStartup(); + } + return "500 What do you think you're trying to pull here?"; + } + XHDR(args) { + if (!this.group) { + return "412 No group selected"; + } + + args = args.split(" "); + var header = args[0].toLowerCase(); + var found = false; + var response = "221 Headers abound\n"; + for (let key of this._filterRange(args[1], this.group.keys)) { + if (!this.group[key].headers.has(header)) { + continue; + } + found = true; + response += key + " " + this.group[key].headers.get(header) + "\n"; + } + if (!found) { + return "420 No such article"; + } + response += "."; + return response; + } + XOVER(args) { + if (!this.group) { + return "412 No group selected"; + } + + args = args.split(/ +/, 3); + var response = "224 List of articles\n"; + for (let key of this._filterRange(args[0], this.group.keys)) { + response += key + "\t"; + var article = this.group[key]; + response += + article.headers.get("subject") + + "\t" + + article.headers.get("from") + + "\t" + + article.headers.get("date") + + "\t" + + article.headers.get("message-id") + + "\t" + + (article.headers.get("references") || "") + + "\t" + + article.fullText.replace(/\r?\n/, "\r\n").length + + "\t" + + article.body.split(/\r?\n/).length + + "\t" + + (article.headers.get("xref") || "") + + "\n"; + } + response += ".\n"; + return response; + } + XPAT(args) { + if (!this.group) { + return "412 No group selected"; + } + + /* XPAT header range ... */ + args = args.split(/ +/, 3); + let header = args[0].toLowerCase(); + let regex = wildmat2regex(args[2]); + + let response = "221 Results follow\n"; + for (let key of this._filterRange(args[1], this.group.keys)) { + let article = this.group[key]; + if ( + article.headers.has(header) && + regex.test(article.headers.get(header)) + ) { + response += key + " " + article.headers.get(header) + "\n"; + } + } + return response + "."; + } + + _filterRange(range, keys) { + let dash = range.indexOf("-"); + let low, high; + if (dash < 0) { + low = high = parseInt(range); + } else { + low = parseInt(range.substring(0, dash)); + if (dash < range.length - 1) { + high = range.substring(dash + 1); + } else { + // Everything is less than this. + high = 1.0 / 0.0; + } + } + return keys.filter(function (e) { + return low <= e && e <= high; + }); + } +} + +class NNTP_Giganews_handler extends NNTP_RFC2980_handler { + XHDR(args) { + var header = args.split(" ")[0].toLowerCase(); + if ( + header in ["subject", "from", "xref", "date", "message-id", "references"] + ) { + return super.XHDR(args); + } + return "503 unsupported header field"; + } +} + +class NNTP_RFC4643_extension extends NNTP_RFC2980_handler { + constructor(daemon) { + super(daemon); + + this.extraCommands += "\tAUTHINFO USER\n"; + this.extraCommands += "\tAUTHINFO PASS\n"; + this.expectedUsername = "testnews"; + this.expectedPassword = "newstest"; + this.requireBoth = true; + this.authenticated = false; + this.usernameReceived = false; + } + + AUTHINFO(args) { + if (this.authenticated) { + return "502 Command unavailable"; + } + + var argSplit = args.split(" "); + var action = argSplit[0]; + var param = argSplit[1]; + + if (action == "user") { + if (this.usernameReceived) { + return "502 Command unavailable"; + } + + var expectUsername = this.lastGroupTried + ? this._daemon.groupCredentials[this.lastGroupTried][0] + : this.expectedUsername; + if (param != expectUsername) { + return "481 Authentication failed"; + } + + this.usernameReceived = true; + if (this.requireBoth) { + return "381 Password required"; + } + + this.authenticated = this.lastGroupTried ? this.lastGroupTried : true; + return "281 Authentication Accepted"; + } else if (action == "pass") { + if (!this.requireBoth || !this.usernameReceived) { + return "482 Authentication commands issued out of sequence"; + } + + this.usernameReceived = false; + + var expectPassword = this.lastGroupTried + ? this._daemon.groupCredentials[this.lastGroupTried][1] + : this.expectedPassword; + if (param != expectPassword) { + return "481 Authentication failed"; + } + + this.authenticated = this.lastGroupTried ? this.lastGroupTried : true; + return "281 Authentication Accepted"; + } + return "502 Invalid Command"; + } + LIST(args) { + if (this.authenticated) { + return args ? super.LIST(args) : "502 Invalid command: LIST"; + } + return "480 Authentication required"; + } + GROUP(args) { + if ( + (this._daemon.groupCredentials != null && this.authenticated == args) || + (this._daemon.groupCredentials == null && this.authenticated) + ) { + return super.GROUP(args); + } + if (this._daemon.groupCredentials != null) { + this.lastGroupTried = args; + } + return "480 Authentication required"; + } +} -- cgit v1.2.3