From d2dafc4dbc678ac54b4b510195a635d9f3156bfa Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Sep 2022 21:03:53 -0600 Subject: [PATCH 1/7] Initial NNTP write access support --- core/message.js | 1 + core/servers/content/nntp.js | 367 ++++++++++++++++++++++++++++++++++- 2 files changed, 359 insertions(+), 9 deletions(-) diff --git a/core/message.js b/core/message.js index 3c0b9c00..6a063802 100644 --- a/core/message.js +++ b/core/message.js @@ -55,6 +55,7 @@ const ADDRESS_FLAVOR = { FTN: 'ftn', // FTN style Email: 'email', // From email QWK: 'qwk', // QWK packet + NNTP: 'nntp', // NNTP article POST; often a email address }; const STATE_FLAGS0 = { diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 95ed9326..dbc3febc 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -9,6 +9,7 @@ const { getTransactionDatabase, getModDatabasePath } = require('../../database.j const { getMessageAreaByTag, getMessageConferenceByTag, + persistMessage, } = require('../../message_area.js'); const User = require('../../user.js'); const Errors = require('../../enig_error.js').Errors; @@ -21,6 +22,7 @@ const { } = require('../../string_util.js'); const AnsiPrep = require('../../ansi_prep.js'); const { stripMciColorCodes } = require('../../color_codes.js'); +const ACS = require('../../acs'); // deps const NNTPServerBase = require('nntp-server'); @@ -111,6 +113,37 @@ class NNTPDatabase { let nntpDatabase; +const AuthCommands = 'POST'; + +// these aren't exported by the NNTP module, unfortunantely +const Responses = { + ArticlePostedOk: '240 article posted ok', + + SendArticle: '340 send article to be posted', + + ArticlePostFailed: '441 posting failed', + AuthRequired: '480 authentication required', +}; + +const PostCommand = { + head: 'POST', + validate: /^POST$/i, + + run(session, cmd) { + if (!session.authenticated) { + session.receivingPostArticle = false; // ensure reset + return Responses.AuthRequired; + } + + session.receivingPostArticle = true; + return Responses.SendArticle; + }, + + capability(session, report) { + report.push('POST'); + }, +}; + class NNTPServer extends NNTPServerBase { constructor(options, serverName) { super(options); @@ -125,6 +158,10 @@ class NNTPServer extends NNTPServerBase { } _needAuth(session, command) { + if (AuthCommands.includes(command)) { + return !session.authenticated && !session.authUser; + } + return super._needAuth(session, command); } @@ -132,7 +169,8 @@ class NNTPServer extends NNTPServerBase { const username = session.authinfo_user; const password = session.authinfo_pass; - this.log.trace({ username }, 'Authentication request'); + // :TODO: log IP address on these.... + this.log.debug({ username }, `NNTP authentication request for "${username}"`); return new Promise(resolve => { const user = new User(); @@ -140,17 +178,19 @@ class NNTPServer extends NNTPServerBase { { type: User.AuthFactor1Types.Password, username, password }, err => { if (err) { - // :TODO: Log IP address - this.log.debug( + this.log.warn( { username, reason: err.message }, - 'Authentication failure' + `NNTP authentication failure for "${username}"` ); return resolve(false); } session.authUser = user; - this.log.debug({ username }, 'User authenticated successfully'); + this.log.info( + { username }, + `NTTP authentication success for "${username}"` + ); return resolve(true); } ); @@ -592,15 +632,33 @@ class NNTPServer extends NNTPServerBase { if (!conf) { return false; } - // :TODO: validate ACS const area = getMessageAreaByTag(areaTag, confTag); if (!area) { return false; } - // :TODO: validate ACS - return false; + const acs = new ACS({ client: null, user: session.authUser }); + return acs.hasMessageConfRead(conf) && acs.hasMessageAreaRead(area); + } + + static hasConfAndAreaWriteAccess(session, confTag, areaTag) { + if (Message.isPrivateAreaTag(areaTag)) { + return false; + } + + const conf = getMessageConferenceByTag(confTag); + if (!conf) { + return false; + } + + const area = getMessageAreaByTag(areaTag, confTag); + if (!area) { + return false; + } + + const acs = new ACS({ client: null, user: session.authUser }); + return acs.hasMessageConfWrite(conf) && acs.hasMessageAreaWrite(area); } getGroup(session, groupName, cb) { @@ -919,8 +977,223 @@ class NNTPServer extends NNTPServerBase { areaTag ).replace(/\./g, '_')}`; } + + static _importMessage(session, articleLines, cb) { + asyncWaterfall( + [ + callback => { + return NNTPServer._parseArticleLines(articleLines, callback); + }, + (parsed, callback) => { + // gather some initially important bits + const subject = parsed.header.get('subject'); + const to = parsed.header.get('to'); // non-standard, may be missing + const from = parsed.header.get('from'); + const date = parsed.header.get('date'); + const newsgroups = parsed.header + .get('newsgroups') + .split(',') + .map(ng => { + const [confTag, areaTag] = ng.split('.'); + return { confTag, areaTag }; + }); + + // validate areaTag exists -- currently only a single area/post; no x-posts + // :TODO: look into x-posting + const area = getMessageAreaByTag(newsgroups[0].areaTag); + if (!area) { + return callback( + Errors.DoesNotExist( + `No area by tag "${newsgroups[0].areaTag}" exists!` + ) + ); + } + + // NOTE: Not all ACS checks work with NNTP since we don't have a standard client; + // If a particular ACS requires a |client|, it will return false! + if ( + !NNTPServer.hasConfAndAreaWriteAccess( + session, + area.confTag, + area.areaTag + ) + ) { + return callback( + Errors.AccessDenied( + `No ACS to ${area.confTag}/${area.areaTag}` + ) + ); + } + + if ( + !_.isString(subject) || + !_.isString(from) || + !_.isString(date) || + !Array.isArray(newsgroups) + ) { + return callback( + Errors.Invalid('Missing one or more required article fields') + ); + } + + return callback(null, { + subject, + from, + date, + newsgroups, + to, + parsed, + }); + }, + (msgData, callback) => { + if (msgData.to) { + return callback(null, msgData); + } + + // + // We don't have a 'to' field, try to derive if this is a + // response to a message. If not, just fall back 'All' + // + // 'References' + // - https://www.rfc-editor.org/rfc/rfc5536#section-3.2.10 + // - https://www.rfc-editor.org/rfc/rfc5322.html + // + // 'In-Reply-To' + // - https://www.rfc-editor.org/rfc/rfc5322.html + // + // Both may contain 1:N, "optionally" separated by CFWS; by this + // point in the code, they should be space separated at most. + // + // Each entry is in msg-id format. That is: + // "<" id-left "@" id-right ">" + // + msgData.to = 'All'; // fallback + let parentMessageId = ( + msgData.parsed.header.get('in-reply-to') || + msgData.parsed.header.get('references') || + '' + ) + .split(' ')[0] + .replace(/[\<\>]/g, ''); + + if (parentMessageId) { + let m = /[0-9]+\.([0-9a-f\-]{36})@enigma-bbs/.exec( + parentMessageId + ); + if (m && m[1]) { + const filter = { + resultType: 'messageList', + uuids: m[1], + limit: 1, + }; + + return Message.findMessages(filter, (err, messageList) => { + if (err) { + return callback(err); + } + + // current message/article is a reply to this message: + msgData.to = messageList[0].fromUserName; + msgData.replyToMsgId = messageList[0].replyToMsgId; // may not be present + return callback(null, msgData); + }); + } + } + + return callback(null, msgData); + }, + (msgData, callback) => { + const message = new Message({ + toUserName: msgData.to, + fromUserName: msgData.from, + subject: msgData.subject, + replyToMsgId: msgData.replyToMsgId || 0, + modTimestamp: msgData.date, // moment can generally parse these + // :TODO: inspect Content-Type 'charset' if present & attempt to properly decode if not UTF-8 + message: msgData.parsed.body.slice(0, -1).join('\n'), // remove trailing blank + areaTag: msgData.newsgroups[0].areaTag, + }); + + message.meta.System[Message.SystemMetaNames.ExternalFlavor] = + Message.AddressFlavor.NNTP; + + // :TODO: investigate JAMNTTP clients/etc. + + persistMessage(message, err => { + return callback(err); + }); + }, + ], + err => { + return cb(err); + } + ); + } + + static _parseArticleLines(articleLines, cb) { + // + // Split articleLines into: + // - Header split into N:V pairs + // - Message Body lines + // - + const header = new Map(); + const body = []; + let inHeader = true; + let currentHeaderName; + forEachSeries( + articleLines, + (line, nextLine) => { + if (inHeader) { + if (line === '.' || line === '') { + inHeader = false; + return nextLine(null); + } + + const sep = line.indexOf(':'); + if (sep < 1) { + // at least a single char name + // entries can split across lines -- they will be prefixed with a single space. + if ( + currentHeaderName && + (line.startsWith(' ') || line.startsWith('\t')) + ) { + let v = header.get(currentHeaderName); + v += line + .replace(/^\t/, ' ') // if we're dealign with a legacy tab + .trimRight(); + header.set(currentHeaderName, v); + return nextLine(null); + } + + return nextLine( + Errors.Invalid( + `"${line}" is not a valid NNTP message header!` + ) + ); + } + + currentHeaderName = line.slice(0, sep).trim().toLowerCase(); + const value = line.slice(sep + 1).trim(); + header.set(currentHeaderName, value); + return nextLine(null); + } + + // body + if (line !== '.') { + // :TODO: de-escape lines that start with ".." -> "." + body.push(line); + } + return nextLine(null); + }, + err => { + return cb(err, { header, body }); + } + ); + } } +const EndOfPostMarker = ['', '.']; + exports.getModule = class NNTPServerModule extends ServerModule { constructor() { super(); @@ -985,9 +1258,85 @@ exports.getModule = class NNTPServerModule extends ServerModule { const config = Config(); + // add in some additional supported commands + const commands = Object.assign({}, NNTPServerBase.commands, { + POST: PostCommand, + }); + + // :TODO: nntp-server doesn't currently allow posting in a nice way, so this is kludged in. Fork+MR something cleaner at some point + class ProxySession extends NNTPServerBase.Session { + constructor(server, stream) { + super(server, stream); + this.articleLinesBuffer = []; + } + + parse(data) { + if (this.receivingPostArticle) { + return this.receivePostArticleData(data); + } + + super.parse(data); + } + + receivePostArticleData(data) { + this.articleLinesBuffer.push(...data.split(/r?\n/)); + + const endOfPost = _.isEqual( + this.articleLinesBuffer.slice(-2), + EndOfPostMarker + ); + if (endOfPost) { + this.receivingPostArticle = false; + + // Command is not exported currently; maybe submit a MR to allow posting in a nicer way... + function Command(runner, articleLines, session) { + this.state = 0; // CMD_WAIT + this.cmd_line = 'POST'; + this.resolved_value = null; + this.rejected_value = null; + this.run = runner; + this.articleLines = articleLines; + this.session = session; + } + + this.pipeline.push( + new Command( + this._processarticleLinesBuffer, + this.articleLinesBuffer, + this + ) + ); + this.articleLinesBuffer = []; + this.tick(); + } + } + + _processarticleLinesBuffer() { + return new Promise(resolve => { + NNTPServer._importMessage(this.session, this.articleLines, err => { + if (err) { + this.rejected_value = Responses.ArticlePostFailed; + this.state = 3; // CMD_REJECTED + } else { + this.resolved_value = Responses.ArticlePostedOk; + this.state = 2; // CMD_RESOLVED + } + + return resolve(); + }); + }); + } + + static create(server, stream) { + return new ProxySession(server, stream); + } + } + const commonOptions = { //requireAuth : true, // :TODO: re-enable! - // :TODO: override |session| - use our own debug to Bunyan, etc. + // :TODO: How to hook into debugging?! + commands, + session: ProxySession, // :TODO: only do this is config.postingAllowed is true, else '440 posting not allowed' even if authenticated }; if (this.enableNntp) { From 2cb0970a319b820b02c05c9b8e3fecbd4436a14a Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Thu, 22 Sep 2022 21:24:24 -0600 Subject: [PATCH 2/7] Add 'allowPosting' config --- core/servers/content/nntp.js | 19 +++++++++++-------- docs/_docs/servers/contentservers/nntp.md | 13 ++++++++++++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index dbc3febc..2ce67e31 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -121,6 +121,7 @@ const Responses = { SendArticle: '340 send article to be posted', + PostingNotAllowed: '440 posting not allowed', ArticlePostFailed: '441 posting failed', AuthRequired: '480 authentication required', }; @@ -1258,11 +1259,6 @@ exports.getModule = class NNTPServerModule extends ServerModule { const config = Config(); - // add in some additional supported commands - const commands = Object.assign({}, NNTPServerBase.commands, { - POST: PostCommand, - }); - // :TODO: nntp-server doesn't currently allow posting in a nice way, so this is kludged in. Fork+MR something cleaner at some point class ProxySession extends NNTPServerBase.Session { constructor(server, stream) { @@ -1333,12 +1329,19 @@ exports.getModule = class NNTPServerModule extends ServerModule { } const commonOptions = { - //requireAuth : true, // :TODO: re-enable! // :TODO: How to hook into debugging?! - commands, - session: ProxySession, // :TODO: only do this is config.postingAllowed is true, else '440 posting not allowed' even if authenticated }; + if (true === _.get(config, 'contentServers.nntp.allowPosts')) { + // add in some additional supported commands + const commands = Object.assign({}, NNTPServerBase.commands, { + POST: PostCommand, + }); + + commonOptions.commands = commands; + commonOptions.session = ProxySession; + } + if (this.enableNntp) { this.nntpServer = new NNTPServer( // :TODO: according to docs: if connection is non-tls, but behind proxy (assuming TLS termination?!!) then set this to true diff --git a/docs/_docs/servers/contentservers/nntp.md b/docs/_docs/servers/contentservers/nntp.md index 6d15fa17..ffb45343 100644 --- a/docs/_docs/servers/contentservers/nntp.md +++ b/docs/_docs/servers/contentservers/nntp.md @@ -12,6 +12,7 @@ The NNTP *content server* provides access to publicly exposed message conference | `nntp` | :-1: | Configuration block for non-secure NNTP. See Non-Secure NNTP Configuration below. | | `nntps` | :-1: | Configuration block for secure NNTP. See Secure NNTPS Configuration below. | | `publicMessageConferences` | :+1: | A map of *conference tags* to *area tags* that are publicly exposed over NNTP. Anonymous users will get read-only access to these areas. | +| `postingAllowed` | :-1: | Allow posting from authenticated users. See [Write Access](#write-access). ### See Non-Secure NNTP Configuration Under `contentServers.nntp.nntp` the following configuration is allowed: @@ -40,10 +41,20 @@ An example of generating your own cert/key pair: openssl req -newkey rsa:2048 -nodes -keyout ./config/nntps_key.pem -x509 -days 3050 -out ./config/nntps_cert.pem ``` -### Example Configuration +## Write Access +Authenticated users may write messages to a group given the following are true: + +1. They are connected security (NNTPS). This is a strict requirement due to how NNTP authenticates in plain-text otherwise. +2. The authenticated user has write [ACS](../../configuration/acs.md) to the target message conference and area. + +> :warning: Not all [ACS](../../configuration/acs.md) checks can be made over NNTP. Any ACS requiring a "client" will return false (fail), such as `LC` ("is local?"). + +## Example Configuration ```hjson contentServers: { nntp: { + allowPosting: true + publicMessageConferences: { fsxnet: [ // Expose these areas of fsxNet From 5f6d70e460955439f3ca62b4430ede968ffe6a65 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Fri, 23 Sep 2022 11:06:33 -0600 Subject: [PATCH 3/7] Some fixes to NNTP POSTs, logging, etc. --- core/servers/content/nntp.js | 48 ++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 2ce67e31..f6fc82f8 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -980,6 +980,17 @@ class NNTPServer extends NNTPServerBase { } static _importMessage(session, articleLines, cb) { + const tidyFrom = f => { + if (f) { + // remove quotes around name, if present + let m = /^"([^"]+)" <([^>]+)>$/.exec(f); + if (m && m[1] && m[2]) { + f = `${m[1]} <${m[2]}>`; + } + } + return f; + }; + asyncWaterfall( [ callback => { @@ -989,8 +1000,10 @@ class NNTPServer extends NNTPServerBase { // gather some initially important bits const subject = parsed.header.get('subject'); const to = parsed.header.get('to'); // non-standard, may be missing - const from = parsed.header.get('from'); - const date = parsed.header.get('date'); + const from = tidyFrom( + parsed.header.get('from') || parsed.header.get('sender') + ); + const date = parsed.header.get('date'); // if not present we'll use 'now' const newsgroups = parsed.header .get('newsgroups') .split(',') @@ -1029,7 +1042,6 @@ class NNTPServer extends NNTPServerBase { if ( !_.isString(subject) || !_.isString(from) || - !_.isString(date) || !Array.isArray(newsgroups) ) { return callback( @@ -1111,7 +1123,7 @@ class NNTPServer extends NNTPServerBase { replyToMsgId: msgData.replyToMsgId || 0, modTimestamp: msgData.date, // moment can generally parse these // :TODO: inspect Content-Type 'charset' if present & attempt to properly decode if not UTF-8 - message: msgData.parsed.body.slice(0, -1).join('\n'), // remove trailing blank + message: msgData.parsed.body.join('\n'), areaTag: msgData.newsgroups[0].areaTag, }); @@ -1121,6 +1133,11 @@ class NNTPServer extends NNTPServerBase { // :TODO: investigate JAMNTTP clients/etc. persistMessage(message, err => { + if (!err) { + Log.info( + `NNTP post to "${message.areaTag}" by "${session.authUser.username}": "${message.subject}"` + ); + } return callback(err); }); }, @@ -1181,8 +1198,12 @@ class NNTPServer extends NNTPServerBase { // body if (line !== '.') { - // :TODO: de-escape lines that start with ".." -> "." - body.push(line); + // lines consisting of a single '.' are escaped to '..' + if (line.startsWith('..')) { + body.push(line.slice(1)); + } else { + body.push(line); + } } return nextLine(null); }, @@ -1193,8 +1214,6 @@ class NNTPServer extends NNTPServerBase { } } -const EndOfPostMarker = ['', '.']; - exports.getModule = class NNTPServerModule extends ServerModule { constructor() { super(); @@ -1277,10 +1296,7 @@ exports.getModule = class NNTPServerModule extends ServerModule { receivePostArticleData(data) { this.articleLinesBuffer.push(...data.split(/r?\n/)); - const endOfPost = _.isEqual( - this.articleLinesBuffer.slice(-2), - EndOfPostMarker - ); + const endOfPost = data.length === 1 && data[0] === '.'; if (endOfPost) { this.receivingPostArticle = false; @@ -1311,8 +1327,14 @@ exports.getModule = class NNTPServerModule extends ServerModule { return new Promise(resolve => { NNTPServer._importMessage(this.session, this.articleLines, err => { if (err) { - this.rejected_value = Responses.ArticlePostFailed; + this.rejected_value = err; // will be serialized and 403 sent back currently; not really ideal as we want ArticlePostFailed + // :TODO: tick() needs updated in session.js such that we can write back a proper code this.state = 3; // CMD_REJECTED + + Log.error( + { error: err.message }, + `NNTP post failed: ${err.message}` + ); } else { this.resolved_value = Responses.ArticlePostedOk; this.state = 2; // CMD_RESOLVED From 3155a0cd8156308308fc821aa206fab81cebc33f Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Sep 2022 09:56:16 -0600 Subject: [PATCH 4/7] Fix docs --- docs/_docs/servers/contentservers/nntp.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/_docs/servers/contentservers/nntp.md b/docs/_docs/servers/contentservers/nntp.md index ffb45343..4cfcbdde 100644 --- a/docs/_docs/servers/contentservers/nntp.md +++ b/docs/_docs/servers/contentservers/nntp.md @@ -12,7 +12,7 @@ The NNTP *content server* provides access to publicly exposed message conference | `nntp` | :-1: | Configuration block for non-secure NNTP. See Non-Secure NNTP Configuration below. | | `nntps` | :-1: | Configuration block for secure NNTP. See Secure NNTPS Configuration below. | | `publicMessageConferences` | :+1: | A map of *conference tags* to *area tags* that are publicly exposed over NNTP. Anonymous users will get read-only access to these areas. | -| `postingAllowed` | :-1: | Allow posting from authenticated users. See [Write Access](#write-access). +| `allowPosts` | :-1: | Allow posting from authenticated users. See [Write Access](#write-access). Default is `false`. ### See Non-Secure NNTP Configuration Under `contentServers.nntp.nntp` the following configuration is allowed: @@ -44,8 +44,9 @@ openssl req -newkey rsa:2048 -nodes -keyout ./config/nntps_key.pem -x509 -days 3 ## Write Access Authenticated users may write messages to a group given the following are true: -1. They are connected security (NNTPS). This is a strict requirement due to how NNTP authenticates in plain-text otherwise. -2. The authenticated user has write [ACS](../../configuration/acs.md) to the target message conference and area. +1. `allowPosts` is set to `true` +2. They are connected security (NNTPS). This is a strict requirement due to how NNTP authenticates in plain-text otherwise. +3. The authenticated user has write [ACS](../../configuration/acs.md) to the target message conference and area. > :warning: Not all [ACS](../../configuration/acs.md) checks can be made over NNTP. Any ACS requiring a "client" will return false (fail), such as `LC` ("is local?"). @@ -53,7 +54,7 @@ Authenticated users may write messages to a group given the following are true: ```hjson contentServers: { nntp: { - allowPosting: true + allowPosts: true publicMessageConferences: { fsxnet: [ From 1626db3d529bd78c75347a97a1c0bd042a26e4b1 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Sep 2022 10:01:27 -0600 Subject: [PATCH 5/7] Clean up NNTP docs --- docs/_docs/servers/contentservers/nntp.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/_docs/servers/contentservers/nntp.md b/docs/_docs/servers/contentservers/nntp.md index 4cfcbdde..59378ef3 100644 --- a/docs/_docs/servers/contentservers/nntp.md +++ b/docs/_docs/servers/contentservers/nntp.md @@ -6,15 +6,17 @@ title: NNTP Server The NNTP *content server* provides access to publicly exposed message conferences and areas over either **secure** NNTPS (NNTP over TLS or nttps://) and/or non-secure NNTP (nntp://). ## Configuration +The following keys are available within the `contentServers.nntp` configuration block: + | Item | Required | Description | |------|----------|-------------| -| `nntp` | :-1: | Configuration block for non-secure NNTP. See Non-Secure NNTP Configuration below. | -| `nntps` | :-1: | Configuration block for secure NNTP. See Secure NNTPS Configuration below. | -| `publicMessageConferences` | :+1: | A map of *conference tags* to *area tags* that are publicly exposed over NNTP. Anonymous users will get read-only access to these areas. | -| `allowPosts` | :-1: | Allow posting from authenticated users. See [Write Access](#write-access). Default is `false`. +| `nntp` | :-1: | Configuration block for non-secure NNTP. See [Non-Secure NNTP Configuration](#non-secure-configuration). | +| `nntps` | :-1: | Configuration block for secure NNTP. See [Secure Configuration (NNTPS)](#secure-configuration-nntps) | +| `publicMessageConferences` | :+1: | A map of *conference tags* to *area tags* that are publicly exposed over NNTP. Anonymous users will gain read-only access to these areas. | +| `allowPosts` | :-1: | Allow posting from authenticated users. See [Write Access](#write-access). Default is `false`. -### See Non-Secure NNTP Configuration +### Non-Secure Configuration Under `contentServers.nntp.nntp` the following configuration is allowed: | Item | Required | Description | @@ -22,7 +24,7 @@ Under `contentServers.nntp.nntp` the following configuration is allowed: | `enabled` | :+1: | Set to `true` to enable non-secure NNTP access. | | `port` | :-1: | Override the default port of `8119`. | -### Secure NNTPS Configuration +### Secure Configuration (NNTPS) Under `contentServers.nntp.nntps` the following configuration is allowed: | Item | Required | Description | @@ -35,7 +37,7 @@ Under `contentServers.nntp.nntps` the following configuration is allowed: #### Certificates and Keys In order to use secure NNTPS, a TLS certificate and key pair must be provided. You may generate your own but most clients **will not trust** them. A certificate and key from a trusted Certificate Authority is recommended. [Let's Encrypt](https://letsencrypt.org/) provides free TLS certificates. Certificates and private keys must be in [PEM format](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail). -##### Generating Your Own +##### Generating a Certificate & Key Pair An example of generating your own cert/key pair: ```bash openssl req -newkey rsa:2048 -nodes -keyout ./config/nntps_key.pem -x509 -days 3050 -out ./config/nntps_cert.pem From c4518c7b9498d4f2da9b99207ae746395ad31873 Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Sep 2022 14:00:52 -0600 Subject: [PATCH 6/7] Tidy/DRY --- core/servers/content/nntp.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index f6fc82f8..2580e573 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -273,6 +273,7 @@ class NNTPServer extends NNTPServerBase { message.nntpHeaders = { From: this.getJAMStyleFrom(message, fromName), 'X-Comment-To': toName, + To: toName, // JAM-ish Newsgroups: session.group.name, Subject: message.subject, Date: this.getMessageDate(message), @@ -384,7 +385,7 @@ class NNTPServer extends NNTPServerBase { messageUuid = msg && msg.messageUuid; } else { // request - [, messageUuid] = this.getMessageIdentifierParts(messageId); + [, messageUuid] = NNTPServer.getMessageIdentifierParts(messageId); } if (!_.isString(messageUuid)) { @@ -920,7 +921,7 @@ class NNTPServer extends NNTPServerBase { return this.makeMessageIdentifier(message.messageId, message.messageUuid); } - getMessageIdentifierParts(messageId) { + static getMessageIdentifierParts(messageId) { const m = messageId.match( /<([0-9]+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})@enigma-bbs>/ ); @@ -999,9 +1000,11 @@ class NNTPServer extends NNTPServerBase { (parsed, callback) => { // gather some initially important bits const subject = parsed.header.get('subject'); - const to = parsed.header.get('to'); // non-standard, may be missing + const to = parsed.header.get('to') || parsed.header.get('x-jam-to'); // non-standard, may be missing const from = tidyFrom( - parsed.header.get('from') || parsed.header.get('sender') + parsed.header.get('from') || + parsed.header.get('sender') || + parsed.header.get('x-jam-from') ); const date = parsed.header.get('date'); // if not present we'll use 'now' const newsgroups = parsed.header @@ -1085,18 +1088,15 @@ class NNTPServer extends NNTPServerBase { msgData.parsed.header.get('in-reply-to') || msgData.parsed.header.get('references') || '' - ) - .split(' ')[0] - .replace(/[\<\>]/g, ''); + ).split(' ')[0]; if (parentMessageId) { - let m = /[0-9]+\.([0-9a-f\-]{36})@enigma-bbs/.exec( - parentMessageId - ); - if (m && m[1]) { + let [_, messageUuid] = + NNTPServer.getMessageIdentifierParts(parentMessageId); + if (messageUuid) { const filter = { resultType: 'messageList', - uuids: m[1], + uuids: messageUuid, limit: 1, }; @@ -1131,6 +1131,7 @@ class NNTPServer extends NNTPServerBase { Message.AddressFlavor.NNTP; // :TODO: investigate JAMNTTP clients/etc. + // :TODO: slurp in various X-XXXX kludges/etc. and bring them in persistMessage(message, err => { if (!err) { From 8c92f3cc4922d12738a1baee6c3247f122a265cb Mon Sep 17 00:00:00 2001 From: Bryan Ashby Date: Sun, 25 Sep 2022 18:29:00 -0600 Subject: [PATCH 7/7] Log IPs --- core/servers/content/nntp.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/core/servers/content/nntp.js b/core/servers/content/nntp.js index 2580e573..9bede3ec 100644 --- a/core/servers/content/nntp.js +++ b/core/servers/content/nntp.js @@ -166,12 +166,19 @@ class NNTPServer extends NNTPServerBase { return super._needAuth(session, command); } + _address(session) { + const addr = session.in_stream.remoteAddress; + return addr ? addr.replace(/^::ffff:/, '').replace(/^::1$/, 'localhost') : 'N/A'; + } + _authenticate(session) { const username = session.authinfo_user; const password = session.authinfo_pass; - // :TODO: log IP address on these.... - this.log.debug({ username }, `NNTP authentication request for "${username}"`); + this.log.debug( + { username, ip: this._address(session) }, + `NNTP authentication request for "${username}"` + ); return new Promise(resolve => { const user = new User(); @@ -180,7 +187,7 @@ class NNTPServer extends NNTPServerBase { err => { if (err) { this.log.warn( - { username, reason: err.message }, + { username, reason: err.message, ip: this._address(session) }, `NNTP authentication failure for "${username}"` ); return resolve(false); @@ -189,7 +196,7 @@ class NNTPServer extends NNTPServerBase { session.authUser = user; this.log.info( - { username }, + { username, ip: this._address(session) }, `NTTP authentication success for "${username}"` ); return resolve(true); @@ -436,7 +443,7 @@ class NNTPServer extends NNTPServerBase { ) ) { this.log.info( - { messageUuid, messageId }, + { messageUuid, messageId, ip: this._address(session) }, 'Access denied for message' ); return resolve(null);