UNPKG

nntp-server-zefie

Version:

NNTP server implementation, modified for zefie_wtvp_minisrv

251 lines (199 loc) 6.76 kB
// Client session. Contains all info about current connection state. // 'use strict'; const crypto = require('crypto'); const Denque = require('denque'); const debug_err = require('debug')('nntp-server.error'); const debug_net = require('debug')('nntp-server.network'); const serializeError = require('serialize-error').serializeError; const split2 = require('split2'); const pipeline = require('stream').pipeline; const flattenStream = require('./flatten-stream'); const status = require('./status'); const CMD_WAIT = 0; const CMD_PENDING = 1; const CMD_RESOLVED = 2; const CMD_REJECTED = 3; // same as lodash.escapeRegExp function escape_regexp(str) { return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&'); } function Command(fn, cmd_line) { this.state = CMD_WAIT; this.fn = fn; this.cmd_line = cmd_line; this.resolved_value = null; this.rejected_value = null; } Command.prototype.run = function () { this.state = CMD_PENDING; return this.fn().then( value => { this.state = CMD_RESOLVED; this.resolved_value = value; }, value => { this.state = CMD_REJECTED; this.rejected_value = value; } ); }; function Session(server, stream) { this.in_stream = stream; this.out_stream = flattenStream(); this.server = server; // Could be just {}, but this is more clean // if this.groups.name is not set, group is not selected this.group = { min_index: 0, max_index: 0, total: 0, name: null, description: '', current_article: 0 }; this.pipeline = new Denque(); this.debug_mark = crypto.pseudoRandomBytes(3).toString('hex'); // Random string used to track connection in logs debug_net(' [%s] %s', this.debug_mark, 'new connection'); // Create RE to search command name. Longest first (for subcommands) let commands = Object.keys(this.server.commands).sort().reverse(); this.__search_cmd_re = RegExp(`^(${commands.map(escape_regexp).join('|')})`, 'i'); this.lines = split2(); this.write((server.options.allow_posting) ? status._200_SRV_READY_RW : status._201_SRV_READY_RO); pipeline(stream, this.lines, () => {}); pipeline(this.out_stream, stream, () => {}); if (debug_net.enabled) { let debug_logger = split2(); pipeline(this.out_stream, debug_logger, () => {}); debug_logger.on('data', line => { debug_net('<-- [%s] %s', this.debug_mark, line); }); } this.lines.on('data', line => { debug_net('--> [%s] %s', this.debug_mark, line); this.parse(line); }); this.lines.on('error', err => { debug_err('ERROR: %O', serializeError(err)); this.server._onError(err); this.out_stream.destroy(); }); this.lines.on('end', () => { debug_net(' [%s] %s', this.debug_mark, 'connection closed'); this.out_stream.destroy(); }); } // By default connection is not secure Session.prototype.secure = false; // Default mode is "reader" Session.prototype.reader = true; Session.prototype.authenticated = false; Session.prototype.authinfo_user = null; Session.prototype.authinfo_pass = null; Session.prototype.current_group = null; /** * Send strings to connected client, adding CRLF after each * * data: * * - String * - Stream of strings (object mode) * - null (close session) * - Array with any combinations above */ Session.prototype.write = function (data) { if (!this.out_stream.writable) { if (typeof data.destroy === 'function') data.destroy(); return; } this.out_stream.write(data); }; function enqueue(session, command) { session.pipeline.push(command); session.tick(); } // Parse client commands and push into pipeline // Session.prototype.parse = function (data) { let input = data.toString().replace(/\r?\n$/, ''); if (input == "POST") { if (this.posting_allowed) { this.is_posting = true // handle post seperately enqueue(this, new Command(() => Promise.resolve(status._340_SEND_ARTICLE), null)) return; } else { enqueue(this, new Command(() => Promise.resolve(status._440_NO_POST_PERM), null)) return; } } if (!this.is_posting) { // Command not recognized if (!this.__search_cmd_re.test(input)) { enqueue(this, new Command(() => Promise.resolve(status._500_CMD_UNKNOWN), input)); return; } let cmd = input.match(this.__search_cmd_re)[1].toUpperCase(); // Command looks known, but whole validation failed -> bad params if (!this.server.commands[cmd].validate.test(input)) { enqueue(this, new Command(() => Promise.resolve(status._501_SYNTAX_ERROR), input)); return; } // Command require auth, but it was not done yet // Force secure connection if needed if (this.server._needAuth(this, cmd)) { enqueue(this, new Command(() => Promise.resolve( this.secure ? status._480_AUTH_REQUIRED : status._483_NOT_SECURE ), input)); return; } enqueue(this, new Command(() => Promise.resolve(this.server.commands[cmd].run(this, input)), input)); } else { if (!this.post_data) this.post_data = {}; if (data.toString().replace(/\r?\n$/, '') == '' && !this.recv_post_body) { this.recv_post_body = true; return; } if (input === ".") { delete this.is_posting; delete this.recv_post_body; if (this.server._postArticle(this)) enqueue(this, new Command(() => Promise.resolve(status._240_ARTICLE_RECV_OK), null)) else enqueue(this, new Command(() => Promise.resolve(status._441_POSTING_FAILED), null)) delete this.post_data; return; } if (!this.recv_post_body) { // headers if (!this.post_data.headers) this.post_data.headers = {} var header = data.toString().split(/\: (.*)/s) this.post_data.headers[header[0]] = header[1]; } else { if (!this.post_data.body) this.post_data.body = []; this.post_data.body.push((data) ? data.toString() : ''); } } }; Session.prototype.tick = function () { if (this.pipeline.isEmpty()) return; let cmd = this.pipeline.peekFront(); if (cmd.state === CMD_RESOLVED) { this.write(cmd.resolved_value); this.pipeline.shift(); this.tick(); } else if (cmd.state === CMD_REJECTED) { if (cmd.rejected_value) cmd.rejected_value.nntp_command = cmd.cmd_line; this.write(status._403_FUCKUP); debug_err('ERROR: %O', serializeError(cmd.rejected_value)); this.server._onError(cmd.rejected_value); this.pipeline.shift(); this.tick(); } else if (cmd.state === CMD_WAIT) { // stop executing commands on closed connection if (!this.out_stream.writable) return; cmd.run().then(() => this.tick()); } }; module.exports = Session; module.exports.create = function (server, stream) { return new Session(server, stream); };