Skip to content

Commit

Permalink
Implement HDR, OVER, LAST, NEXT, NEWNEWS commands
Browse files Browse the repository at this point in the history
  • Loading branch information
rlidwka committed Apr 26, 2017
1 parent 4c4648f commit 8ed1093
Show file tree
Hide file tree
Showing 20 changed files with 846 additions and 70 deletions.
69 changes: 62 additions & 7 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
// TODO
// - STARTTLS
// - newnews
// - xhdr
// - xover
//
// - Session.write(stream) + general rework
// - Optimize Range headers get.

Expand Down Expand Up @@ -129,7 +125,7 @@ Nntp.prototype._authenticate = function (/*session*/) {
*
* - message_id: number-like string or '<message_identifier>'
*/
Nntp.prototype._getMessage = function (/*session, message_id*/) {
Nntp.prototype._getArticle = function (/*session, message_id*/) {
return Promise.resolve(null);
};

Expand All @@ -145,6 +141,16 @@ Nntp.prototype._getRange = function (/*session, first, last, options*/) {
/*
* Try to select group by name. Returns `true` on success
* and fill `session.group` data.
*
* `session.group` data is an object:
*
* - min_index (Number) - low water mark
* - max_index (Number) - high water mark
* - total (Number) - an amount of messages in the group
* - name (Number) - group name, e.g. 'misc.test'
* - description (String) - group description (optional)
* - current_article (Number) - usually equals to min_index, can be modified
* by the server later, 0 means invalid
*/
Nntp.prototype._selectGroup = function (/*session, name*/) {
return Promise.resolve(false);
Expand Down Expand Up @@ -175,10 +181,59 @@ Nntp.prototype._buildHead = function (/*session, message*/) {
Nntp.prototype._buildBody = function (/*session, message*/) {
};


/*
* Generate message id - '<xxxxx@xxxx.xxx>'
* Generate header content
*
* NNTP server user may request any field using HDR command,
* and in addition to that the following fields are used internally by
* nntp-server:
*
* - subject
* - from
* - date
* - message-id
* - references
* - :bytes
* - :lines
* - xref
*/
Nntp.prototype._buildHeaderField = function (/*session, message, field*/) {
};


/*
* Get fields for OVER and LIST OVERVIEW.FMT commands.
*
* First 7 fields (up to :lines) are mandatory and should not be changed,
* you can remove Xref or add any field supported by buildHeaderField after
* that.
*
* Format matches LIST OVERVIEW.FMT, ':full' means header includes header
* name itself (which is mandatory for custom fields).
*/
Nntp.prototype._getOverviewFmt = function (/*session*/) {
return [
'Subject:',
'From:',
'Date:',
'Message-ID:',
'References:',
':bytes',
':lines',
'Xref:full'
];
};


/*
* Get list of messages newer than specified timestamp
* in NNTP groups selected by a wildcard.
*
* - time - minimal last update time
* - wildmat - name filter
*/
Nntp.prototype._buildId = function (/*session, message*/) {
Nntp.prototype._getNewNews = function (/*session, time, wildmat*/) {
};


Expand Down
19 changes: 14 additions & 5 deletions lib/commands/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,34 @@ module.exports = {

run(session, cmd) {
let match = cmd.match(CMD_RE);
let id;

// Requests without id not implemented (no need, not used by clients)
if (!match[1]) return status._501_SYNTAX_ERROR;
if (!match[1]) {
let cursor = session.group.current_article;

if (cursor <= 0) return status._420_ARTICLE_NOT_SLCTD;

id = cursor.toString();
} else {
id = match[2];
}

let id = match[2];
let by_identifier = id[0] === '<';

if (!by_identifier && !session.group.name) {
return status._412_GRP_NOT_SLCTD;
}

return session.server._getMessage(session, id)
return session.server._getArticle(session, id)
.then(msg => {
if (!msg) {
if (by_identifier) return status._430_NO_ARTICLE_BY_ID;
return status._423_NO_ARTICLE_BY_NUM;
}

let msg_id = session.server._buildId(session, msg);
if (!by_identifier) session.group.current_article = msg.index;

let msg_id = session.server._buildHeaderField(session, msg, 'message-id');
let msg_index = by_identifier ? 0 : id;

return [
Expand Down
19 changes: 14 additions & 5 deletions lib/commands/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,34 @@ module.exports = {

run(session, cmd) {
let match = cmd.match(CMD_RE);
let id;

// Requests without id not implemented (no need, not used by clients)
if (!match[1]) return status._501_SYNTAX_ERROR;
if (!match[1]) {
let cursor = session.group.current_article;

if (cursor <= 0) return status._420_ARTICLE_NOT_SLCTD;

id = cursor.toString();
} else {
id = match[2];
}

let id = match[2];
let by_identifier = id[0] === '<';

if (!by_identifier && !session.group.name) {
return status._412_GRP_NOT_SLCTD;
}

return session.server._getMessage(session, id)
return session.server._getArticle(session, id)
.then(msg => {
if (!msg) {
if (by_identifier) return status._430_NO_ARTICLE_BY_ID;
return status._423_NO_ARTICLE_BY_NUM;
}

let msg_id = session.server._buildId(session, msg);
if (!by_identifier) session.group.current_article = msg.index;

let msg_id = session.server._buildHeaderField(session, msg, 'message-id');
let msg_index = by_identifier ? 0 : id;

return [
Expand Down
15 changes: 7 additions & 8 deletions lib/commands/capabilities.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// https://tools.ietf.org/html/rfc3977#section-5.2
//
'use strict';


const status = require('../status');
const CRLF = '\r\n';


module.exports = {
Expand Down Expand Up @@ -38,12 +38,11 @@ module.exports = {
}
});

return [
status._101_CAPABILITY_LIST,
Object.keys(uniq)
.map(k => [ k ].concat(uniq[k]).join(' '))
.join(CRLF),
'.'
];
return [ status._101_CAPABILITY_LIST ]
.concat(
Object.keys(uniq)
.map(k => [ k ].concat(uniq[k]).join(' '))
)
.concat([ '.' ]);
}
};
13 changes: 6 additions & 7 deletions lib/commands/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,15 @@ module.exports = {
head: 'GROUP',
validate: CMD_RE,

run(session, cmd) {
async run(session, cmd) {
let name = cmd.match(CMD_RE)[1];

return session.server._selectGroup(session, name)
.then(ok => {
if (!ok) return status._411_GRP_NOT_FOUND;
let ok = await session.server._selectGroup(session, name);

let g = session.group;
if (!ok) return status._411_GRP_NOT_FOUND;

return `${status._211_GRP_SELECTED} ${g.total} ${g.min_index} ${g.max_index} ${name}`;
});
let g = session.group;

return `${status._211_GRP_SELECTED} ${g.total} ${g.min_index} ${g.max_index} ${name}`;
}
};
81 changes: 81 additions & 0 deletions lib/commands/hdr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// https://tools.ietf.org/html/rfc3977#section-8.5
//
'use strict';


const status = require('../status');


const CMD_RE = /^X?HDR ([^\s]+)(?: (?:(\d{1,15})(-(\d{1,15})?)?|(<[^\s<>]+>))?)?$/i;

module.exports = {
head: 'HDR',
validate: CMD_RE,

async run(session, cmd) {
let [ , field, first, dash, last, message_id ] = cmd.match(CMD_RE);

field = field.toLowerCase();

let result = [ status._225_HEADERS_FOLLOW ];
let list;

if (typeof message_id !== 'undefined') {
let msg = await session.server._getArticle(session, message_id);

if (!msg) return status._430_NO_ARTICLE_BY_ID;

list = [ msg ];

} else if (typeof first !== 'undefined') {
first = +first;

if (!dash) {
last = first;
} else {
last = typeof last === 'undefined' ? session.group.max_index : +last;
}

if (!session.group.name) return status._412_GRP_NOT_SLCTD;

list = await session.server._getRange(session, first, last);

// TODO: status text is slightly wrong
if (!list.length) return status._423_NO_ARTICLE_BY_NUM;

} else {
if (session.group.current_article <= 0) return status._420_ARTICLE_NOT_SLCTD;

let msg = await session.server._getArticle(session, String(session.group.current_article));

if (!msg) return status._420_ARTICLE_NOT_SLCTD;

list = [ msg ];
}

result = result.concat(list.map(msg => {
let index;

if (typeof message_id !== 'undefined') {
index = '0';
} else {
index = msg.index.toString();
}

let content = (session.server._buildHeaderField(session, msg, field) || '');

// unfolding + replacing invalid characters, see RFC 3977 section 8.3.2
content = content.replace(/\r?\n/g, '').replace(/[\0\t\r\n]/g, ' ');

return index + ' ' + content;
}));

result.push('.');

return result;
},

capability(session, report) {
report.push('HDR');
}
};
19 changes: 14 additions & 5 deletions lib/commands/head.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,34 @@ module.exports = {

run(session, cmd) {
let match = cmd.match(CMD_RE);
let id;

// Requests without id not implemented (no need, not used by clients)
if (!match[1]) return status._501_SYNTAX_ERROR;
if (!match[1]) {
let cursor = session.group.current_article;

if (cursor <= 0) return status._420_ARTICLE_NOT_SLCTD;

id = cursor.toString();
} else {
id = match[2];
}

let id = match[2];
let by_identifier = id[0] === '<';

if (!by_identifier && !session.group.name) {
return status._412_GRP_NOT_SLCTD;
}

return session.server._getMessage(session, id)
return session.server._getArticle(session, id)
.then(msg => {
if (!msg) {
if (by_identifier) return status._430_NO_ARTICLE_BY_ID;
return status._423_NO_ARTICLE_BY_NUM;
}

let msg_id = session.server._buildId(session, msg);
if (!by_identifier) session.group.current_article = msg.index;

let msg_id = session.server._buildHeaderField(session, msg, 'message-id');
let msg_index = by_identifier ? 0 : id;

return [
Expand Down
40 changes: 40 additions & 0 deletions lib/commands/list_newsgroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// https://tools.ietf.org/html/rfc3977#section-7.6.6
//
'use strict';


const status = require('../status');
const wildmat_re = require('../wildmat');


const CMD_RE = /^LIST NEWSGROUPS( ([^\s]+))?$/i;


module.exports = {
head: 'LIST NEWSGROUPS',
validate: CMD_RE,
pipeline: true,

run(session, cmd) {
let wildmat = null;

if (cmd.match(CMD_RE)[2]) {
try {
wildmat = wildmat_re(cmd.match(CMD_RE)[2]);
} catch (err) {
return `501 ${err.message}`;
}
}

return session.server._getGroups(session, 0, wildmat)
.then(groups =>
[ status._215_INFO_FOLLOWS ]
.concat(groups.map(g => `${g.name}\t${g.description || ''}`))
.concat([ '.' ])
);
},

capability(session, report) {
report.push([ 'LIST', 'NEWSGROUPS' ]);
}
};
Loading

0 comments on commit 8ed1093

Please sign in to comment.