UNPKG

nntp-fast

Version:

A lightweight nntp client for node made with attention to speed and ease of use. Works with promises and streams.

757 lines (754 loc) 32 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); var net = require("net"); var tls = require("tls"); var stream_1 = require("stream"); var util = require("util"); var StreamSearch = require("streamsearch"); var DotUnstuffingStreamSearch_1 = require("./DotUnstuffingStreamSearch"); var events_1 = require("events"); var Delimiter; (function (Delimiter) { Delimiter[Delimiter["CRLF"] = 0] = "CRLF"; Delimiter[Delimiter["MLDB"] = 1] = "MLDB"; // multiline datablock \r\n.\r\n })(Delimiter || (Delimiter = {})); var RES_CODE_ML = [100, 101, 215, 220, 221, 222, 224, 225, 230, 231]; /** * @noInheritDoc */ var NntpConnection = /** @class */ (function (_super) { __extends(NntpConnection, _super); /** * * @param options.dotUnstuffing Whether dot unstuffing should be performed. */ function NntpConnection(options) { var _this = _super.call(this) || this; /** @private */ _this._responseQueue = []; /** @private */ _this._buffer = []; /** @private */ _this._delimiterSearchType = Delimiter.CRLF; if (!options) options = {}; if (options.dotUnstuffing === undefined) options.dotUnstuffing = true; _this._streamsearchCRLF = new StreamSearch(Buffer.from("\r\n")); if (options.dotUnstuffing) { _this._streamsearchMLDB = new DotUnstuffingStreamSearch_1.default(Buffer.from("\r\n.\r\n")); } else { _this._streamsearchMLDB = new StreamSearch(Buffer.from("\r\n.\r\n")); } _this._streamsearchCRLF.maxMatches = 1; _this._streamsearchMLDB.maxMatches = 1; _this._streamsearchCRLF.on("info", _this._onInfoCRLF.bind(_this)); _this._streamsearchMLDB.on("info", _this._onInfoMLDB.bind(_this)); return _this; } NntpConnection.prototype.connect = function (hostOrOptions, portOrSecure, secure) { var _this = this; if (this._connected) { return Promise.resolve(this._connectedResponse); } var options; if (typeof hostOrOptions === "string" && typeof portOrSecure === "number") { options = { host: hostOrOptions, port: portOrSecure }; } else if (typeof hostOrOptions == "object" && (typeof portOrSecure == "boolean" || portOrSecure == undefined)) { options = hostOrOptions; secure = portOrSecure; } else { throw new TypeError(); } return new Promise(function (resolve, reject) { // Initial Handler var handler = { handleResponse: function (code, message) { _this._connected = true; _this._connectedResponse = { code: code, message: message }; resolve({ code: code, message: message }); } }; _this._responseQueue = [handler]; if (secure) { _this._socket = tls.connect(options, function () { // 'connect' listener. }); } else { _this._socket = net.connect(options, function () { // 'connect' listener. }); } _this._socket.on('data', function (data) { _this._onData(data); }); _this._socket.on('end', function () { _this._connected = false; _this.emit("end"); }); _this._socket.on('error', function (err) { _this._connected = false; reject(); _this.emit("error", err); }); _this._socket.on('timeout', function () { _this._connected = false; _this._socket.end(); _this.emit("timeout"); }); }); }; /** * @private * @param data */ NntpConnection.prototype._onData = function (data) { var r; if (this._delimiterSearchType == Delimiter.CRLF) { r = this._streamsearchCRLF.push(data); if (this._streamsearchCRLF.matches == 1) { if (this._handleResponse(Buffer.concat(this._buffer).toString())) { // returns true if multi line data block is expected this._delimiterSearchType = Delimiter.MLDB; } else { this._responseQueue.splice(0, 1); } this._buffer = []; this._streamsearchCRLF.reset(); } } else { r = this._streamsearchMLDB.push(data); if (this._streamsearchMLDB.matches == 1) { var stream = this._responseQueue[0].MldbStream; stream === null || stream === void 0 ? void 0 : stream.end(); this._responseQueue.splice(0, 1); this._delimiterSearchType = Delimiter.CRLF; this._streamsearchMLDB.reset(); } } if (r < data.length) { // Data after delimiter this._onData(data.slice(r)); } }; /** * @private * @param isMatch * @param data * @param start * @param end */ NntpConnection.prototype._onInfoCRLF = function (isMatch, data, start, end) { if (data) { if (start == 0 && end == data.length) { this._buffer.push(data); } else { this._buffer.push(data.slice(start, end)); } } // Note: never call reset() inside this callback }; /** * @private * @param isMatch * @param data * @param start * @param end */ NntpConnection.prototype._onInfoMLDB = function (isMatch, data, start, end) { var stream = this._responseQueue[0].MldbStream; if (data) { if (start == 0 && end == data.length) { stream === null || stream === void 0 ? void 0 : stream.write(data); } else { stream === null || stream === void 0 ? void 0 : stream.write(data.slice(start, end)); } } // Note: never call reset() inside this callback }; /** * @private * @param response */ NntpConnection.prototype._getCode = function (response) { var res = /^([0-9]{3}) ([^\r\n]*)$/.exec(response); if (res) { return { code: parseInt(res[1]), message: res[2] }; } else { throw new Error("couldnt parse response: " + response); } }; /** * @private * @param headString */ NntpConnection.prototype._parseHeader = function (headString) { var headers = {}; for (var _i = 0, _a = headString.split("\r\n"); _i < _a.length; _i++) { var entry = _a[_i]; var firstColon = entry.indexOf(': '); headers[entry.substr(0, firstColon)] = entry.substr(firstColon + 2); } return headers; }; /** * @private * @param response */ NntpConnection.prototype._handleResponse = function (response) { var _a; var _b = this._getCode(response), code = _b.code, message = _b.message; var responseHandler = this._responseQueue[0]; if (this._responseQueue.length == 0) { this.emit("error", new Error("Unexpected response: " + code + " " + message)); return false; } if (RES_CODE_ML.includes(code) || (responseHandler.decideMldb && responseHandler.decideMldb(code))) { if (responseHandler.MldbStream) { responseHandler.handleResponse(code, message); } else { // mldb not expected but given: wait for end before calling callback responseHandler.MldbStream = new stream_1.PassThrough(); var responseData_1 = []; responseHandler.MldbStream.on("data", function (data) { responseData_1.push(data); }); responseHandler.MldbStream.on("end", function () { responseHandler.handleResponse(code, message, Buffer.concat(responseData_1)); }); } return true; } else { responseHandler.handleResponse(code, message); (_a = responseHandler.MldbStream) === null || _a === void 0 ? void 0 : _a.end(); return false; } }; /** * Sends command to server. * @param command command to execute * @param decideMldb optional function that decides from response code whether a * multi line data block is to be expected */ NntpConnection.prototype.runCommand = function (command, decideMldb) { var _this = this; return new Promise(function (resolve) { var handler = { handleResponse: function (code, message, data) { resolve({ code: code, message: message, data: data }); }, decideMldb: decideMldb }; _this._responseQueue.push(handler); _this._socket.write(command + "\r\n"); }); }; /** * Sends command to server. Creates a stream for the multi line data block. * @param command command to execute * @param decideMldb optional function that decides from response code whether a * multi line data block is to be expected */ NntpConnection.prototype.runCommandStream = function (command, decideMldb) { var _this = this; var stream = new stream_1.PassThrough(); var response = new Promise(function (resolve) { var handler = { handleResponse: function (code, message) { resolve({ code: code, message: message }); }, MldbStream: stream, decideMldb: decideMldb }; _this._responseQueue.push(handler); _this._socket.write(command + "\r\n"); }); return { stream: stream, response: response }; }; /** * The CAPABILITIES command allows a client to determine the * capabilities of the server at any given time. * * See [RFC 3977 Section 3.3](https://tools.ietf.org/html/rfc3977#section-3.3) * @param keyword */ NntpConnection.prototype.capabilities = function (keyword) { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("CAPABILITIES" + (keyword ? " " + keyword : ""))]; case 1: res = _a.sent(); if (res.code == 101) { return [2 /*return*/, res]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The MODE READER command instructs a mode-switching server to switch * modes, as described in [RFC 3977 Section 3.4.2](https://tools.ietf.org/html/rfc3977#section-3.4.2). */ NntpConnection.prototype.modeReader = function () { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("MODE READER")]; case 1: res = _a.sent(); if ([200, 201 /*, 502*/].includes(res.code)) { return [2 /*return*/, res]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The client uses the QUIT command to terminate the session. */ NntpConnection.prototype.quit = function () { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("QUIT")]; case 1: res = _a.sent(); if (205 == res.code) { return [2 /*return*/, res]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The GROUP command selects a newsgroup as the currently selected * newsgroup and returns summary information about it. * @param group */ NntpConnection.prototype.group = function (group) { return __awaiter(this, void 0, void 0, function () { var res, m; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("GROUP " + group)]; case 1: res = _a.sent(); if (res.code == 211) { m = /^([0-9]+) ([0-9]+) ([0-9]+) (.+)$/.exec(res.message); if (m == null) { throw "cant parse " + util.inspect(res.message); } return [2 /*return*/, { code: res.code, number: parseInt(m[1]), low: parseInt(m[2]), high: parseInt(m[3]), group: m[4] }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The LISTGROUP command selects a newsgroup in the same manner as the * GROUP command but also provides a list of article * numbers in the newsgroup. If no group is specified, the currently * selected newsgroup is used. * * @param group * @param range */ NntpConnection.prototype.listgroup = function (group, range) { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("LISTGROUP" + (group ? " " + group + (range ? " " + range : "") : ""), function (code) { return code == 211; })]; case 1: res = _a.sent(); if (res.code == 211) { // let m = /^([0-9]+) ([0-9]+) ([0-9]+) (.+)$/.exec(res.message); // ^ not on all servers supportes if (!res.data) throw new Error("no data on listgroup"); return [2 /*return*/, { code: res.code, message: res.message, articles: res.data.toString().split("\r\n").map(function (n) { return parseInt(n); }) }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The current article number will be set to the previous article in * that newsgroup */ NntpConnection.prototype.last = function () { return __awaiter(this, void 0, void 0, function () { var res, m; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("LAST")]; case 1: res = _a.sent(); if ([223 /*, 412, 420, 422*/].includes(res.code)) { m = /^([0-9]+) (<[^ ]+>)/.exec(res.message); if (m == null) { throw "cant parse " + util.inspect(res.message); } return [2 /*return*/, { code: res.code, articleNumber: parseInt(m[1]), articleId: m[2] }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The current article number will be set to the next article in * that newsgroup */ NntpConnection.prototype.next = function () { return __awaiter(this, void 0, void 0, function () { var res, m; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("NEXT")]; case 1: res = _a.sent(); if ([223 /*, 412, 420, 422*/].includes(res.code)) { m = /^([0-9]+) (<[^ ]+>)/.exec(res.message); if (m == null) { throw "cant parse " + util.inspect(res.message); } return [2 /*return*/, { code: res.code, articleNumber: parseInt(m[1]), articleId: m[2] }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The ARTICLE command selects an article according to the arguments and * presents the entire article (that is, the headers, and the body) to * the client. * * @param messageid */ NntpConnection.prototype.article = function (messageid) { return __awaiter(this, void 0, void 0, function () { var res, headBodySeparation; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("ARTICLE" + (messageid ? " " + messageid : ""))]; case 1: res = _a.sent(); if ([220 /*, 430, 412, 423, 420*/].includes(res.code)) { if (!res.data) throw new Error("no data on article"); headBodySeparation = res.data.indexOf("\r\n\r\n"); return [2 /*return*/, { code: res.code, headers: this._parseHeader(res.data.slice(0, headBodySeparation).toString()), body: res.data.slice(headBodySeparation + 4) }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The HEAD command selects an article according to the arguments and * presents the headers to the client. * * @param messageid */ NntpConnection.prototype.head = function (messageid) { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("HEAD" + (messageid ? " " + messageid : ""))]; case 1: res = _a.sent(); if ([221 /*, 430, 412, 423, 420*/].includes(res.code)) { if (!res.data) throw new Error("no data on head"); return [2 /*return*/, { code: res.code, headers: this._parseHeader(res.data.toString()) }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The BODY command selects an article according to the arguments and * presents the body to the client. * * @param messageid */ NntpConnection.prototype.body = function (messageid) { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("BODY" + (messageid ? " " + messageid : ""))]; case 1: res = _a.sent(); if ([222 /*, 430, 412, 423, 420*/].includes(res.code)) { if (!res.data) throw new Error("no data on body"); return [2 /*return*/, { code: res.code, body: res.data }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * The BODY command selects an article according to the arguments and * presents the body to the client. Body is given as stream. * @param messageid */ NntpConnection.prototype.bodyStream = function (messageid) { return this.runCommandStream("BODY" + (messageid ? " " + messageid : "")); }; /** * The STAT command selects an article according to the arguments. * This command allows the client to determine whether an article exists * and what its message-id is, without having to process an arbitrary * amount of text. * * @param messageid */ NntpConnection.prototype.stat = function (messageid) { return __awaiter(this, void 0, void 0, function () { var res, m; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("STAT" + (messageid ? " " + messageid : ""))]; case 1: res = _a.sent(); if ([223 /*, 430, 412, 423, 420*/].includes(res.code)) { m = /^([0-9]+) (<[^ ]+>)/.exec(res.message); if (m == null) { throw "cant parse " + util.inspect(res.message); } return [2 /*return*/, { code: res.code, articleNumber: parseInt(m[1]), articleId: m[2] }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /*async post(headers: Headers, data: Buffer): Promise<BasicResponse> { throw new Error("Not implemented"); } async ihave(message_id: string, headers: Headers, data: Buffer): Promise<BasicResponse> { throw new Error("Not implemented"); }*/ /** * This command exists to help clients find out the current Coordinated * Universal Time from the server's perspective. */ NntpConnection.prototype.date = function () { return __awaiter(this, void 0, void 0, function () { var res, date; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("DATE")]; case 1: res = _a.sent(); if (res.code == 111) { date = /^([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})/.exec(res.message); if (!date) throw new Error("cant parse date: " + res.message); return [2 /*return*/, { code: res.code, date: new Date(Date.UTC(parseInt(date[1]), parseInt(date[2]) - 1, parseInt(date[3]), parseInt(date[4]), parseInt(date[5]), parseInt(date[6]))) }]; } else { throw res; } return [2 /*return*/]; } }); }); }; /** * This command provides a short summary of the commands that are * understood by this implementation of the server. */ NntpConnection.prototype.help = function () { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this.runCommand("HELP")]; case 1: res = _a.sent(); if (res.code == 100) { return [2 /*return*/, res]; } else { throw res; } return [2 /*return*/]; } }); }); }; NntpConnection.prototype.newsgroups = function (date, time, gmt) { return __awaiter(this, void 0, void 0, function () { var res; return __generator(this, function (_a) { switch (_a.label) { case 0: if (date instanceof Date) { time = date.getUTCHours().toString().padStart(2, "0") + date.getUTCMinutes().toString().padStart(2, "0") + date.getUTCSeconds().toString().padStart(2, "0"); date = date.getUTCFullYear().toString().padStart(4, "0") + (date.getUTCMonth() + 1).toString().padStart(2, "0") + date.getUTCDay().toString().padStart(2, "0"); gmt = true; } if (!time) { time = "000000"; } return [4 /*yield*/, this.runCommand("NEWGROUPS " + date + " " + time + (gmt ? " GMT" : ""))]; case 1: res = _a.sent(); if (res.code == 231) { return [2 /*return*/, res]; } else { throw res; } return [2 /*return*/]; } }); }); }; return NntpConnection; }(events_1.EventEmitter)); exports.NntpConnection = NntpConnection;