diff --git a/core/config.js b/core/config.js index dc8f3a8e..61de4244 100644 --- a/core/config.js +++ b/core/config.js @@ -172,7 +172,10 @@ function getDefaultConfig() { paths : { mods : paths.join(__dirname, './../mods/'), servers : paths.join(__dirname, './servers/'), - msgNetworks : paths.join(__dirname, './msg_networks/'), + + scannerTossers : paths.join(__dirname, './scanner_tossers/'), + mailers : paths.join(__dirname, './mailers/') , + art : paths.join(__dirname, './../mods/art/'), themes : paths.join(__dirname, './../mods/themes/'), logs : paths.join(__dirname, './../logs/'), // :TODO: set up based on system, e.g. /var/logs/enigmabbs or such diff --git a/core/fse.js b/core/fse.js index 69c5e418..317485e9 100644 --- a/core/fse.js +++ b/core/fse.js @@ -307,9 +307,9 @@ function FullScreenEditorModule(options) { // :TODO: We'd like to delete up to N rows, but this does not work // in NetRunner: - //self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); + self.client.term.rawWrite(ansi.reset() + ansi.deleteLine(3)); - self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)) + //self.client.term.rawWrite(ansi.reset() + ansi.eraseLine(2)) } callback(null); }, diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index a9603f2e..8b69228d 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -17,39 +17,37 @@ var buffers = require('buffers'); var moment = require('moment'); /* - :TODO: should probably be broken up - FTNPacket - FTNPacketImport: packet -> message(s) - FTNPacketExport: message(s) -> packet -*/ + :TODO: things + * Read/detect packet types: 2, 2.2, and 2+ + * Write packet types: 2, 2.2, and 2+ + * Test SAUCE ignore/extraction + * FSP-1010 for netmail (see SBBS) -/* -Reader: file to ftn data -Writer: ftn data to packet -Data to toMessage -Data.fromMessage - -FTNMessage.toMessage() => Message -FTNMessage.fromMessage() => Create from Message - -* read: header -> simple {} obj, msg -> Message object -* read: read(..., iterator): iterator('header', ...), iterator('message', msg) -* write: provide information to go into header - -* Logic of "Is this for us"/etc. elsewhere */ const FTN_PACKET_HEADER_SIZE = 58; // fixed header size const FTN_PACKET_HEADER_TYPE = 2; const FTN_PACKET_MESSAGE_TYPE = 2; +const FTN_PACKET_BAUD_TYPE_2_2 = 2; -// EOF + SAUCE.id + SAUCE.version ('00') -const FTN_MESSAGE_SAUCE_HEADER = - new Buffer( [ 0x1a, 'S', 'A', 'U', 'C', 'E', '0', '0' ] ); +// SAUCE magic header + version ("00") +const FTN_MESSAGE_SAUCE_HEADER = new Buffer('SAUCE00'); const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; +// +// Read/Write FTN packets with support for the following formats: +// +// * Type 1 FTS-0001 @ http://ftsc.org/docs/fts-0001.016 (Obsolete) +// * Type 2.2 FSC-0045 @ http://ftsc.org/docs/fsc-0045.001 +// * Type 2+ FSC-0039 and FSC-0048 @ http://ftsc.org/docs/fsc-0039.004 +// and http://ftsc.org/docs/fsc-0048.002 +// +// Additional resources: +// * Writeup on differences between type 2, 2.2, and 2+: +// http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt +// function FTNPacket() { var self = this; @@ -57,16 +55,14 @@ function FTNPacket() { this.parsePacketHeader = function(packetBuffer, cb) { assert(Buffer.isBuffer(packetBuffer)); - // - // See the following specs: - // http://ftsc.org/docs/fts-0001.016 - // http://ftsc.org/docs/fsc-0048.002 - // if(packetBuffer.length < FTN_PACKET_HEADER_SIZE) { cb(new Error('Buffer too small')); return; } + // + // Start out reading as if this is a FSC-0048 2+ packet + // binary.parse(packetBuffer) .word16lu('origNode') .word16lu('destNode') @@ -81,7 +77,7 @@ function FTNPacket() { .word16lu('origNet') .word16lu('destNet') .word8('prodCodeLo') - .word8('revisionMajor') // aka serialNo + .word8('prodRevLo') // aka serialNo .buffer('password', 8) // null padded C style string .word16lu('origZone') .word16lu('destZone') @@ -89,9 +85,9 @@ function FTNPacket() { .word16lu('auxNet') .word16lu('capWordA') .word8('prodCodeHi') - .word8('revisionMinor') + .word8('prodRevHi') .word16lu('capWordB') - .word16lu('originZone2') + .word16lu('origZone2') .word16lu('destZone2') .word16lu('originPoint') .word16lu('destPoint') @@ -104,6 +100,30 @@ function FTNPacket() { cb(new Error('Unsupported header type: ' + packetHeader.packetType)); return; } + + // + // What kind of packet do we really have here? + // + if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { + packetHeader.packetVersion = '2.2'; + } else { + // + // See heuristics described in FSC-0048, "Receiving Type-2+ bundles" + // + const capWordASwapped = + ((packetHeader.capWordA & 0xff) << 8) | + ((packetHeader.capWordA >> 8) & 0xff); + + if(capWordASwapped === packetHeader.capWordB && + 0 != packetHeader.capWordB && + packetHeader.capWordB & 0x0001) + { + packetHeader.packetVersion = '2+'; + } else { + packetHeader.packetVersion = '2'; + packetHeader.point + } + } // // Date/time components into something more reasonable @@ -131,7 +151,7 @@ function FTNPacket() { buffer.writeUInt16LE(headerInfo.origNet, 20); buffer.writeUInt16LE(headerInfo.destNet, 22); buffer.writeUInt8(headerInfo.prodCodeLo, 24); - buffer.writeUInt8(headerInfo.revisionMajor, 25); + buffer.writeUInt8(headerInfo.prodRevHi, 25); const pass = ftn.stringToNullPaddedBuffer(headerInfo.password, 8); pass.copy(buffer, 26); @@ -143,7 +163,7 @@ function FTNPacket() { buffer.writeUInt16LE(headerInfo.auxNet, 38); buffer.writeUInt16LE(headerInfo.capWordA, 40); buffer.writeUInt8(headerInfo.prodCodeHi, 42); - buffer.writeUInt8(headerInfo.revisionMinor, 43); + buffer.writeUInt8(headerInfo.prodRevLo, 43); buffer.writeUInt16LE(headerInfo.capWordB, 44); buffer.writeUInt16LE(headerInfo.origZone2, 46); buffer.writeUInt16LE(headerInfo.destZone2, 48); @@ -207,13 +227,16 @@ function FTNPacket() { // :TODO: This is wrong: SAUCE may not have EOF marker for one, also if it's // present, we need to extract it but keep the rest of hte message intact as it likely // has SEEN-BY, PATH, and other kludge information *appended* - const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER); + const sauceHeaderPosition = messageBodyBuffer.indexOf(FTN_MESSAGE_SAUCE_HEADER); if(sauceHeaderPosition > -1) { - sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition), (err, theSauce) => { + sauce.readSAUCE(messageBodyBuffer.slice(sauceHeaderPosition, sauceHeaderPosition + sauce.SAUCE_SIZE), (err, theSauce) => { if(!err) { // we read some SAUCE - don't re-process that portion into the body - messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); + messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition) + messageBodyBuffer.slice(sauceHeaderPosition + sauce.SAUCE_SIZE); +// messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition); messageBodyData.sauce = theSauce; + } else { + console.log(err) } callback(null); // failure to read SAUCE is OK }); @@ -349,6 +372,19 @@ function FTNPacket() { msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine; } + // + // Update message UUID, if possible, based on MSGID and AREA + // + if(_.isString(msg.meta.FtnKludge.MSGID) && + _.isString(msg.meta.FtnProperty.ftn_area) && + msg.meta.FtnKludge.MSGID.length > 0 && + msg.meta.FtnProperty.ftn_area.length > 0) + { + msg.uuid = ftn.createMessageUuid( + msg.meta.FtnKludge.MSGID, + msg.meta.FtnProperty.area); + } + iterator('message', msg); }) }); @@ -367,21 +403,6 @@ function FTNPacket() { basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); - // - // From http://ftsc.org/docs/fts-0001.016: - // DateTime = (* a character string 20 characters long *) - // (* 01 Jan 86 02:34:56 *) - // DayOfMonth " " Month " " Year " " - // " " HH ":" MM ":" SS - // Null - // - // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) - // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | - // "Jul" | "Aug" | "Sep" | "Oct" | "Nov" | "Dec" - // Year = "01" | "02" | .. | "85" | "86" | ... | "99" | "00" - // HH = "00" | .. | "23" - // MM = "00" | .. | "59" - // SS = "00" | .. | "59" const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); dateTimeBuffer.copy(basicHeader, 14); @@ -423,12 +444,17 @@ function FTNPacket() { } } - // :TODO: is Area really any differnt (e.g. no space between AREA:the_area) + // + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // AREA:CONFERENCE + // Should be first line in a message + // if(message.meta.FtnProperty.ftn_area) { msgBody += `AREA:${message.meta.FtnProperty.ftn_area}\n`; } Object.keys(message.meta.FtnKludge).forEach(k => { + // we want PATH to be last if('PATH' !== k) { appendMeta(k, message.meta.FtnKludge[k]); } @@ -436,9 +462,21 @@ function FTNPacket() { msgBody += message.message; + // + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // Origin line should be near the bottom of a message + // appendMeta('', message.meta.FtnProperty.ftn_tear_line); + + // + // Tear line should be near the bottom of a message + // appendMeta('', message.meta.FtnProperty.ftn_origin); + // + // FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001 + // SEEN-BY and PATH should be the last lines of a message + // appendMeta('SEEN-BY', message.meta.FtnProperty.ftn_seen_by); appendMeta('PATH', message.meta.FtnKludge['PATH']); @@ -509,498 +547,6 @@ FTNPacket.prototype.write = function(path, headerInfo, messages, cb) { }; - -// -// References -// * http://ftsc.org/docs/fts-0001.016 -// * http://ftsc.org/docs/fsc-0048.002 -// -// Other implementations: -// * https://github.com/M-griffin/PyPacketMail/blob/master/PyPacketMail.py -// -function FTNMailPacket(options) { - - //MailPacket.call(this, options); - - var self = this; - self.KLUDGE_PREFIX = '\x01'; - - this.getPacketHeaderAddress = function() { - return { - zone : self.packetHeader.destZone, - net : self.packetHeader.destNet, - node : self.packetHeader.destNode, - point : self.packetHeader.destPoint, - }; - }; - - this.getNetworkNameForAddress = function(addr) { - var nodeAddr; - for(var network in self.nodeAddresses) { - nodeAddr = self.nodeAddresses[network]; - if(nodeAddr.zone === addr.zone && - nodeAddr.net === addr.net && - nodeAddr.node === addr.node && - nodeAddr.point === addr.point) - { - return network; - } - } - }; - - this.parseFtnPacketHeader = function(packetBuffer, cb) { - assert(Buffer.isBuffer(packetBuffer)); - - if(packetBuffer.length < 58) { - cb(new Error('Buffer too small')); - return; - } - - binary.parse(packetBuffer) - .word16lu('origNode') - .word16lu('destNode') - .word16lu('year') - .word16lu('month') - .word16lu('day') - .word16lu('hour') - .word16lu('minute') - .word16lu('second') - .word16lu('baud') - .word16lu('packetType') - .word16lu('origNet') - .word16lu('destNet') - .word8('prodCodeLo') - .word8('revisionMajor') // aka serialNo - .buffer('password', 8) // null terminated C style string - .word16lu('origZone') - .word16lu('destZone') - // Additions in FSC-0048.002 follow... - .word16lu('auxNet') - .word16lu('capWordA') - .word8('prodCodeHi') - .word8('revisionMinor') - .word16lu('capWordB') - .word16lu('originZone2') - .word16lu('destZone2') - .word16lu('originPoint') - .word16lu('destPoint') - .word32lu('prodData') - .tap(function tapped(packetHeader) { - packetHeader.password = ftn.stringFromFTN(packetHeader.password); - - // :TODO: Don't hard code magic # here - if(2 !== packetHeader.packetType) { - console.log(packetHeader.packetType) - cb(new Error('Packet is not Type-2')); - return; - } - - // :TODO: convert date information -> .created - - packetHeader.created = moment(packetHeader); - /* - packetHeader.year, packetHeader.month, packetHeader.day, packetHeader.hour, - packetHeader.minute, packetHeader.second);*/ - - // :TODO: validate & pass error if failure - cb(null, packetHeader); - }); - }; - - this.getPacketHeaderBuffer = function(packetHeader, options) { - options = options || {}; - - if(options.created) { - options.created = moment(options.created); // ensure we have a moment obj - } else { - options.created = moment(); - } - - let buffer = new Buffer(58); - - buffer.writeUInt16LE(packetHeader.origNode, 0); - buffer.writeUInt16LE(packetHeader.destNode, 2); - buffer.writeUInt16LE(options.created.year(), 4); - buffer.writeUInt16LE(options.created.month(), 6); - buffer.writeUInt16LE(options.created.date(), 8); - buffer.writeUInt16LE(options.created.hour(), 10); - buffer.writeUInt16LE(options.created.minute(), 12); - buffer.writeUInt16LE(options.created.second(), 14); - buffer.writeUInt16LE(0x0000, 16); - buffer.writeUInt16LE(0x0002, 18); - buffer.writeUInt16LE(packetHeader.origNet, 20); - buffer.writeUInt16LE(packetHeader.destNet, 22); - buffer.writeUInt8(packetHeader.prodCodeLo, 24); - buffer.writeUInt8(packetHeader.revisionMajor, 25); - - const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8); - pass.copy(buffer, 26); - - buffer.writeUInt16LE(packetHeader.origZone, 34); - buffer.writeUInt16LE(packetHeader.destZone, 36); - - // FSC-0048.002 additions... - buffer.writeUInt16LE(packetHeader.auxNet, 38); - buffer.writeUInt16LE(packetHeader.capWordA, 40); - buffer.writeUInt8(packetHeader.prodCodeHi, 42); - buffer.writeUInt8(packetHeader.revisionMinor, 43); - buffer.writeUInt16LE(packetHeader.capWordB, 44); - buffer.writeUInt16LE(packetHeader.origZone2, 46); - buffer.writeUInt16LE(packetHeader.destZone2, 48); - buffer.writeUInt16LE(packetHeader.origPoint, 50); - buffer.writeUInt16LE(packetHeader.destPoint, 52); - buffer.writeUInt32LE(packetHeader.prodData, 54); - - return buffer; - }; - - self.setOrAppend = function(value, dst) { - if(dst) { - if(!_.isArray(dst)) { - dst = [ dst ]; - } - - dst.push(value); - } else { - dst = value; - } - } - - - self.getMessageMeta = function(msgBody, msgData) { - var meta = { - FtnKludge : msgBody.kludgeLines, - FtnProperty : {}, - }; - - if(msgBody.tearLine) { - meta.FtnProperty.ftn_tear_line = msgBody.tearLine; - } - if(msgBody.seenBy.length > 0) { - meta.FtnProperty.ftn_seen_by = msgBody.seenBy; - } - if(msgBody.area) { - meta.FtnProperty.ftn_area = msgBody.area; - } - if(msgBody.originLine) { - meta.FtnProperty.ftn_origin = msgBody.originLine; - } - - meta.FtnProperty.ftn_orig_node = msgData.origNode; - meta.FtnProperty.ftn_dest_node = msgData.destNode; - meta.FtnProperty.ftn_orig_network = msgData.origNet; - meta.FtnProperty.ftn_dest_network = msgData.destNet; - meta.FtnProperty.ftn_attr_flags1 = msgData.attrFlags1; - meta.FtnProperty.ftn_attr_flags2 = msgData.attrFlags2; - meta.FtnProperty.ftn_cost = msgData.cost; - - return meta; - }; - - this.parseFtnMessageBody = function(msgBodyBuffer, cb) { - // - // From FTS-0001.16: - // "Message text is unbounded and null terminated (note exception below). - // - // A 'hard' carriage return, 0DH, marks the end of a paragraph, and must - // be preserved. - // - // So called 'soft' carriage returns, 8DH, may mark a previous - // processor's automatic line wrap, and should be ignored. Beware that - // they may be followed by linefeeds, or may not. - // - // All linefeeds, 0AH, should be ignored. Systems which display message - // text should wrap long lines to suit their application." - // - // This is a bit tricky. Decoding the buffer to CP437 converts all 0x8d -> 0xec, so we'll - // have to replace those characters if the buffer is left as CP437. - // After decoding, we'll need to peek at the buffer for the various kludge lines - // for charsets & possibly re-decode. Uggh! - // - - // :TODO: Use the proper encoding here. There appear to be multiple specs and/or - // stuff people do with this... some specs kludge lines, which is kinda durpy since - // to get to that point, one must read the file (and decode) to find said kludge... - - - //var msgLines = msgBodyBuffer.toString().split(/\r\n|[\n\v\f\r\x85\u2028\u2029]/g); - - //var msgLines = iconv.decode(msgBodyBuffer, 'CP437').replace(/\xec/g, '').split(/\r\n|[\r\n]/g); - var msgLines = iconv.decode(msgBodyBuffer, 'CP437').replace(/[\xec\n]/g, '').split(/\r/g); - - var msgBody = { - message : [], - kludgeLines : {}, // -> [ value1, value2, ... ] - seenBy : [], - }; - - var preOrigin = true; - - function addKludgeLine(kl) { - const kludgeParts = kl.split(':'); - kludgeParts[0] = kludgeParts[0].toUpperCase(); - kludgeParts[1] = kludgeParts[1].trim(); - - self.setOrAppend(kludgeParts[1], msgBody.kludgeLines[kludgeParts[0]]); - } - - var sauceBuffers; - - msgLines.forEach(function nextLine(line) { - if(0 === line.length) { - msgBody.message.push(''); - return; - } - - if(preOrigin) { - if(_.startsWith(line, 'AREA:')) { - msgBody.area = line.substring(line.indexOf(':') + 1).trim(); - } else if(_.startsWith(line, '--- ')) { - // Tag lines are tracked allowing for specialized display/etc. - msgBody.tearLine = line; - } else if(/[ ]{1,2}(\* )?Origin\: /.test(line)) { // To spec is " * Origin: ..." - msgBody.originLine = line; - preOrigin = false; - } else if(self.KLUDGE_PREFIX === line.charAt(0)) { - addKludgeLine(line.slice(1)); - } else if(!sauceBuffers || _.startsWith(line, '\x1aSAUCE00')) { - sauceBuffers = sauceBuffers || buffers(); - sauceBuffers.push(new Buffer(line)); - } else { - msgBody.message.push(line); - } - } else { - if(_.startsWith(line, 'SEEN-BY:')) { - msgBody.seenBy.push(line.substring(line.indexOf(':') + 1).trim()); - } else if(self.KLUDGE_PREFIX === line.charAt(0)) { - addKludgeLine(line.slice(1)); - } - } - }); - - if(sauceBuffers) { - // :TODO: parse sauce -> sauce buffer. This needs changes to this method to return message & optional sauce - } - - cb(null, msgBody); - }; - - this.extractMessages = function(buffer, iterator, cb) { - assert(Buffer.isBuffer(buffer)); - assert(_.isFunction(iterator)); - - const NULL_TERM_BUFFER = new Buffer( [ 0 ] ); - - binary.stream(buffer).loop(function looper(end, vars) { - this - .word16lu('messageType') - .word16lu('origNode') - .word16lu('destNode') - .word16lu('origNet') - .word16lu('destNet') - .word8('attrFlags1') - .word8('attrFlags2') - .word16lu('cost') - .scan('modDateTime', NULL_TERM_BUFFER) - .scan('toUserName', NULL_TERM_BUFFER) - .scan('fromUserName', NULL_TERM_BUFFER) - .scan('subject', NULL_TERM_BUFFER) - .scan('message', NULL_TERM_BUFFER) - .tap(function tapped(msgData) { - if(!msgData.origNode) { - end(); - cb(null); - return; - } - - // buffer to string conversion - [ 'modDateTime', 'toUserName', 'fromUserName', 'subject', ].forEach(function field(f) { - msgData[f] = iconv.decode(msgData[f], 'CP437'); - }); - - self.parseFtnMessageBody(msgData.message, function msgBodyParsed(err, msgBody) { - // - // Now, create a Message object - // - var msg = new Message( { - // AREA FTN -> local conf/area occurs elsewhere - toUserName : msgData.toUserName, - fromUserName : msgData.fromUserName, - subject : msgData.subject, - message : msgBody.message.join('\n'), // :TODO: \r\n is better? - modTimestamp : ftn.getDateFromFtnDateTime(msgData.modDateTime), - meta : self.getMessageMeta(msgBody, msgData), - - - }); - - iterator(msg); - //self.emit('message', msg); // :TODO: Placeholder - }); - }); - }); - }; - - //this.getMessageHeaderBuffer = function(headerInfo) - - this.parseFtnMessages = function(buffer, cb) { - var nullTermBuf = new Buffer( [ 0 ] ); - var fidoMessages = []; - - binary.stream(buffer).loop(function looper(end, vars) { - this - .word16lu('messageType') - .word16lu('origNode') - .word16lu('destNode') - .word16lu('origNet') - .word16lu('destNet') - .word8('attrFlags1') - .word8('attrFlags2') - .word16lu('cost') - .scan('modDateTime', nullTermBuf) - .scan('toUserName', nullTermBuf) - .scan('fromUserName', nullTermBuf) - .scan('subject', nullTermBuf) - .scan('message', nullTermBuf) - .tap(function tapped(msgData) { - if(!msgData.origNode) { - end(); - cb(null, fidoMessages); - return; - } - - // buffer to string conversion - // :TODO: What is the real encoding here? - [ 'modDateTime', 'toUserName', 'fromUserName', 'subject', ].forEach(function field(f) { - msgData[f] = msgData[f].toString(); - }); - - self.parseFtnMessageBody(msgData.message, function msgBodyParsed(err, msgBody) { - msgData.message = msgBody; - fidoMessages.push(_.clone(msgData)); - }); - }); - }); - }; - - this.extractMesssagesFromPacketBuffer = function(packetBuffer, iterator, cb) { - assert(Buffer.isBuffer(packetBuffer)); - assert(_.isFunction(iterator)); - - async.waterfall( - [ - function parseHeader(callback) { - self.parseFtnPacketHeader(packetBuffer, function headerParsed(err, packetHeader) { - self.packetHeader = packetHeader; - callback(err); - }); - }, - function validateDesinationAddress(callback) { - self.localNetworkName = self.getNetworkNameForAddress(self.getPacketHeaderAddress()); - self.localNetworkName = 'AllowAnyNetworkForDebugging'; - callback(self.localNetworkName ? null : new Error('Packet not addressed do this system')); - }, - function extractEmbeddedMessages(callback) { - // note: packet header is 58 bytes in length - self.extractMessages( - packetBuffer.slice(58), iterator, function extracted(err) { - callback(err); - }); - } - ], - function complete(err) { - cb(err); - } - ); - }; - - this.loadMessagesFromPacketBuffer = function(packetBuffer, cb) { - async.waterfall( - [ - function parseHeader(callback) { - self.parseFtnPacketHeader(packetBuffer, function headerParsed(err, packetHeader) { - self.packetHeader = packetHeader; - callback(err); - }); - }, - function validateDesinationAddress(callback) { - self.localNetworkName = self.getNetworkNameForAddress(self.getPacketHeaderAddress()); - self.localNetworkName = 'AllowAnyNetworkForDebugging'; - callback(self.localNetworkName ? null : new Error('Packet not addressed do this system')); - }, - function parseMessages(callback) { - self.parseFtnMessages(packetBuffer.slice(58), function messagesParsed(err, fidoMessages) { - callback(err, fidoMessages); - }); - }, - function createMessageObjects(fidoMessages, callback) { - fidoMessages.forEach(function msg(fmsg) { - console.log(fmsg); - }); - } - ], - function complete(err) { - cb(err); - } - ); - }; -} - -//require('util').inherits(FTNMailPacket, MailPacket); - -FTNMailPacket.prototype.parse = function(path, cb) { - var self = this; - - async.waterfall( - [ - function readFromFile(callback) { - fs.readFile(path, function packetData(err, data) { - callback(err, data); - }); - }, - function extractMessages(data, callback) { - self.loadMessagesFromPacketBuffer(data, function extracted(err, messages) { - callback(err, messages); - }); - } - ], - function complete(err, messages) { - cb(err, messages); - } - ); -}; - -FTNMailPacket.prototype.read = function(pathOrBuffer, iterator, cb) { - var self = this; - - if(_.isString(pathOrBuffer)) { - async.waterfall( - [ - function readPacketFile(callback) { - fs.readFile(pathOrBuffer, function packetData(err, data) { - callback(err, data); - }); - }, - function extractMessages(data, callback) { - self.extractMesssagesFromPacketBuffer(data, iterator, callback); - } - ], - cb - ); - } else if(Buffer.isBuffer(pathOrBuffer)) { - - } -}; - -FTNMailPacket.prototype.write = function(messages, fileName, options) { - if(!_.isArray(messages)) { - messages = [ messages ]; - } - - - -}; - var ftnPacket = new FTNPacket(); var theHeader; var written = false; @@ -1023,67 +569,23 @@ ftnPacket.read( }); } + + let address = { + zone : 46, + net : 1, + node : 232, + domain : 'l33t.codes', + }; + msg.areaTag = 'agn_bbs'; + msg.messageId = 1234; + console.log(ftn.getMessageIdentifier(msg, address)); + console.log(ftn.getProductIdentifier()) + //console.log(ftn.getOrigin(address)) + console.log(ftn.parseAddress('46:1/232.4@l33t.codes')) + console.log(ftn.getUTCTimeZoneOffset()) } }, function completion(err) { console.log(err); } ); - -/* -var mailPacket = new FTNMailPacket( - { - nodeAddresses : { - fidoNet : { - zone : 46, - net : 1, - node : 140, - point : 0, - domain : '' - } - } - } -); - - -var didWrite = false; -mailPacket.read( - process.argv[2], - //'/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/mf/extracted/27000425.pkt', - function packetIter(msg) { - console.log(msg); - if(_.has(msg, 'meta.FtnProperty.ftn_area')) { - console.log('AREA: ' + msg.meta.FtnProperty.ftn_area); - } - - if(!didWrite) { - console.log(mailPacket.packetHeader); - console.log('-----------'); - - - didWrite = true; - - let outTest = fs.createWriteStream('/home/nuskooler/Downloads/ftnout/test1.pkt'); - let buffer = mailPacket.getPacketHeaderBuffer(mailPacket.packetHeader); - //mailPacket.write(buffer, msg.packetHeader); - outTest.write(buffer); - } - }, - function complete(err) { - console.log(err); - } -); -*/ -/* - Area Map - networkName: { - area_tag: conf_name:area_tag_name - ... - } -*/ - -/* -mailPacket.parse('/home/nuskooler/ownCloud/Projects/ENiGMA½ BBS/FTNPackets/BAD_BNDL.007', function parsed(err, messages) { - console.log(err) -}); -*/ \ No newline at end of file diff --git a/core/ftn_util.js b/core/ftn_util.js index 41c8dfc6..94f716f2 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -11,16 +11,37 @@ var fs = require('fs'); var util = require('util'); var iconv = require('iconv-lite'); var moment = require('moment'); +var createHash = require('crypto').createHash; +var uuid = require('node-uuid'); +var os = require('os'); + +var packageJson = require('../package.json'); // :TODO: Remove "Ftn" from most of these -- it's implied in the module exports.stringFromFTN = stringFromFTN; exports.stringToNullPaddedBuffer = stringToNullPaddedBuffer; -exports.getFormattedFTNAddress = getFormattedFTNAddress; +exports.createMessageUuid = createMessageUuid; +exports.parseAddress = parseAddress; +exports.formatAddress = formatAddress; exports.getDateFromFtnDateTime = getDateFromFtnDateTime; exports.getDateTimeString = getDateTimeString; +exports.getMessageIdentifier = getMessageIdentifier; +exports.getProductIdentifier = getProductIdentifier; +exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; +exports.getOrigin = getOrigin; + exports.getQuotePrefix = getQuotePrefix; +// +// Namespace for RFC-4122 name based UUIDs generated from +// FTN kludges MSGID + AREA +// +const ENIGMA_FTN_MSGID_NAMESPACE = uuid.parse('a5c7ae11-420c-4469-a116-0e9a6d8d2654'); + +// Up to 5D FTN address RegExp +const ENIGMA_FTN_ADDRESS_REGEXP = /^([0-9]+):([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i; + // See list here: https://github.com/Mithgol/node-fidonet-jam // :TODO: proably move this elsewhere as a general method @@ -48,6 +69,7 @@ function stringToNullPaddedBuffer(s, bufLen) { // // Convert a FTN style DateTime string to a Date object // +// :TODO: Name the next couple methods better - for FTN *packets* function getDateFromFtnDateTime(dateTime) { // // Examples seen in the wild (Working): @@ -63,10 +85,10 @@ function getDateTimeString(m) { // // From http://ftsc.org/docs/fts-0001.016: // DateTime = (* a character string 20 characters long *) - // (* 01 Jan 86 02:34:56 *) - // DayOfMonth " " Month " " Year " " - // " " HH ":" MM ":" SS - // Null + // (* 01 Jan 86 02:34:56 *) + // DayOfMonth " " Month " " Year " " + // " " HH ":" MM ":" SS + // Null // // DayOfMonth = "01" | "02" | "03" | ... | "31" (* Fido 0 fills *) // Month = "Jan" | "Feb" | "Mar" | "Apr" | "May" | "Jun" | @@ -83,42 +105,172 @@ function getDateTimeString(m) { return m.format('DD MMM YY HH:mm:ss'); } -function getFormattedFTNAddress(address, dimensions) { - //var addr = util.format('%d:%d', address.zone, address.net); - var addr = '{0}:{1}'.format(address.zone, address.net); - switch(dimensions) { - case 2 : - case '2D' : - // above - break; +function createMessageUuid(ftnMsgId, ftnArea) { + // + // v5 UUID generation code based on the work here: + // https://github.com/download13/uuidv5/blob/master/uuid.js + // + // Note: CrashMail uses MSGID + AREA, so we go with that as well: + // https://github.com/larsks/crashmail/blob/master/crashmail/dupe.c + // + if(!Buffer.isBuffer(ftnMsgId)) { + ftnMsgId = iconv.encode(ftnMsgId, 'CP437'); + } - case 3 : - case '3D' : - addr += '/{0}'.format(address.node); - break; + ftnArea = ftnArea || ''; // AREA is optional + if(!Buffer.isBuffer(ftnArea)) { + ftnArea = iconv.encode(ftnArea, 'CP437'); + } + + const ns = new Buffer(ENIGMA_FTN_MSGID_NAMESPACE); - case 4 : - case '4D': - addr += '.{0}'.format(address.point || 0); // missing and 0 are equiv for point - break; + let digest = createHash('sha1').update( + Buffer.concat([ ns, ftnMsgId, ftnArea ])).digest(); - case 5 : - case '5D' : - if(address.domain) { - addr += '@{0}'.format(address.domain); - } - break; + let u = new Buffer(16); + + // bbbb - bb - bb - bb - bbbbbb + digest.copy(u, 0, 0, 4); // time_low + digest.copy(u, 4, 4, 6); // time_mid + digest.copy(u, 6, 6, 8); // time_hi_and_version + + u[6] = (u[6] & 0x0f) | 0x50; // version, 4 most significant bits are set to version 5 (0101) + u[8] = (digest[8] & 0x3f) | 0x80; // clock_seq_hi_and_reserved, 2msb are set to 10 + u[9] = digest[9]; + + digest.copy(u, 10, 10, 16); + + return uuid.unparse(u); // to string +} + +function parseAddress(address) { + const m = ENIGMA_FTN_ADDRESS_REGEXP.exec(address); + + if(m) { + let addr = { + zone : parseInt(m[1]), + net : parseInt(m[2]), + }; + + // + // substr(1) on the following to remove the + // captured prefix + // + if(m[3]) { + addr.node = parseInt(m[3].substr(1)); + } + + if(m[4]) { + addr.point = parseInt(m[4].substr(1)); + } + + if(m[5]) { + addr.domain = m[5].substr(1); + } + + return addr; + } +} + +function formatAddress(address, dimensions) { + let addr = `${address.zone}:${address.net}`; + + // allow for e.g. '4D' or 5 + const dim = parseInt(dimensions.toString()[0]); + + if(dim >= 3) { + addr += `/${address.node}`; + } + + // missing & .0 are equiv for point + if(dim >= 4 && address.point) { + addr += `.${addresss.point}`; + } + + if(5 === dim && address.domain) { + addr += `@${address.domain.toLowerCase()}`; } return addr; } -function getFtnMessageSerialNumber(messageId) { - return ((Math.floor((Date.now() - Date.UTC(2015, 1, 1)) / 1000) + messageId)).toString(16); +function getMessageSerialNumber(message) { + return ('00000000' + ((Math.floor((Date.now() - Date.UTC(2016, 1, 1)) / 1000) + + message.messageId)).toString(16)).substr(-8); } +// +// Return a FTS-0009.001 compliant MSGID value given a message +// See http://ftsc.org/docs/fts-0009.001 +// +// "A MSGID line consists of the string "^AMSGID:" (where ^A is a +// control-A (hex 01) and the double-quotes are not part of the +// string), followed by a space, the address of the originating +// system, and a serial number unique to that message on the +// originating system, i.e.: +// +// ^AMSGID: origaddr serialno +// +// The originating address should be specified in a form that +// constitutes a valid return address for the originating network. +// If the originating address is enclosed in double-quotes, the +// entire string between the beginning and ending double-quotes is +// considered to be the orginating address. A double-quote character +// within a quoted address is represented by by two consecutive +// double-quote characters. The serial number may be any eight +// character hexadecimal number, as long as it is unique - no two +// messages from a given system may have the same serial number +// within a three years. The manner in which this serial number is +// generated is left to the implementor." +// +// +// Examples & Implementations +// +// Synchronet: .@ +// 2606.agora-agn_tst@46:1/142 19609217 +// +// Mystic: +// 46:3/102 46686263 +// +// ENiGMA½: .@<5dFtnAddress> +// +function getMessageIdentifier(message, address) { + return `${message.messageId}.${message.areaTag.toLowerCase()}@${formatAddress(address, '5D')} ${getMessageSerialNumber(message)}`; +} + +// +// Return a FSC-0046.005 Product Identifier or "PID" +// http://ftsc.org/docs/fsc-0046.005 +// +function getProductIdentifier() { + const version = packageJson.version + .replace(/\-/g, '.') + .replace(/alpha/,'a') + .replace(/beta/,'b'); + + const nodeVer = process.version.substr(1); // remove 'v' prefix + + return `ENiGMA1/2 ${version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; +} + +// +// Return a FSC-0030.001 compliant (http://ftsc.org/docs/fsc-0030.001) MESSAGE-ID +// +// +// +// :TODO: not implemented to spec at all yet :) function getFTNMessageID(messageId, areaId) { - return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getFTNMessageSerialNumber(messageId) + return messageId + '.' + areaId + '@' + getFTNAddress() + ' ' + getMessageSerialNumber(messageId) +} + +// +// Return a FRL-1004 style time zone offset for a +// 'TZUTC' kludge line +// +// http://ftsc.org/docs/frl-1004.002 +// +function getUTCTimeZoneOffset() { + return moment().format('ZZ').replace(/\+/, ''); } // Get a FSC-0032 style quote prefixes @@ -127,25 +279,14 @@ function getQuotePrefix(name) { return ' ' + name[0].toUpperCase() + name[1].toLowerCase() + '> '; } - // -// Specs: -// * http://ftsc.org/docs/fts-0009.001 -// * -// -function getFtnMsgIdKludgeLine(origAddress, messageId) { - if(_.isObject(origAddress)) { - origAddress = getFormattedFTNAddress(origAddress, '5D'); - } +// Return a FTS-0004 Origin line +// http://ftsc.org/docs/fts-0004.001 +// +function getOrigin(address) { + const origin = _.has(Config.messageNetworks.originName) ? + Config.messageNetworks.originName : + Config.general.boardName; - return '\x01MSGID: ' + origAddress + ' ' + getFtnMessageSerialNumber(messageId); -} - - -function getFTNOriginLine() { - // - // Specs: - // http://ftsc.org/docs/fts-0004.001 - // - return ' * Origin: ' + Config.general.boardName + '(' + getFidoNetAddress() + ')'; + return ` * Origin: ${origin} (${formatAddress(address, '5D')})`; } diff --git a/core/message.js b/core/message.js index cdbfe67f..4e0bd0a9 100644 --- a/core/message.js +++ b/core/message.js @@ -17,7 +17,7 @@ function Message(options) { this.messageId = options.messageId || 0; // always generated @ persist this.areaTag = options.areaTag || Message.WellKnownAreaTags.Invalid; - this.uuid = uuid.v1(); + this.uuid = options.uuid || uuid.v1(); this.replyToMsgId = options.replyToMsgId || 0; this.toUserName = options.toUserName || ''; this.fromUserName = options.fromUserName || ''; diff --git a/core/msg_network_module.js b/core/msg_network_module.js deleted file mode 100644 index 8d4a9e0c..00000000 --- a/core/msg_network_module.js +++ /dev/null @@ -1,25 +0,0 @@ -/* jslint node: true */ -'use strict'; - -// ENiGMA½ -var PluginModule = require('./plugin_module.js').PluginModule; - -exports.MessageNetworkModule = MessageNetworkModule; - -function MessageNetworkModule() { - PluginModule.call(this); -} - -require('util').inherits(MessageNetworkModule, PluginModule); - -MessageNetworkModule.prototype.startup = function(cb) { - cb(null); -}; - -MessageNetworkModule.prototype.shutdown = function(cb) { - cb(null); -}; - -MessageNetworkModule.prototype.record = function(message, cb) { - cb(null); -}; \ No newline at end of file diff --git a/core/msg_networks/ftn_msg_network_module.js b/core/msg_networks/ftn_msg_network_module.js deleted file mode 100644 index f81aef9f..00000000 --- a/core/msg_networks/ftn_msg_network_module.js +++ /dev/null @@ -1,27 +0,0 @@ -/* jslint node: true */ -'use strict'; - -// ENiGMA½ -var MessageNetworkModule = require('./msg_network_module.js').MessageNetworkModule; - -function FTNMessageNetworkModule() { - MessageNetworkModule.call(this); -} - -require('util').inherits(FTNMessageNetworkModule, MessageNetworkModule); - -FTNMessageNetworkModule.prototype.startup = function(cb) { - cb(null); -}; - -FTNMessageNetworkModule.prototype.shutdown = function(cb) { - cb(null); -}; - -FTNMessageNetworkModule.prototype.record = function(message, cb) { - cb(null); - - // :TODO: should perhaps record in batches - e.g. start an event, record - // to temp location until time is hit or N achieved such that if multiple - // messages are being created a .FTN file is not made for each one -}; \ No newline at end of file diff --git a/core/msg_scan_toss_module.js b/core/msg_scan_toss_module.js new file mode 100644 index 00000000..e396f44d --- /dev/null +++ b/core/msg_scan_toss_module.js @@ -0,0 +1,25 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +var PluginModule = require('./plugin_module.js').PluginModule; + +exports.MessageScanTossModule = MessageScanTossModule; + +function MessageScanTossModule() { + PluginModule.call(this); +} + +require('util').inherits(MessageScanTossModule, PluginModule); + +MessageScanTossModule.prototype.startup = function(cb) { + cb(null); +}; + +MessageScanTossModule.prototype.shutdown = function(cb) { + cb(null); +}; + +MessageScanTossModule.prototype.record = function(message, cb) { + cb(null); +}; \ No newline at end of file diff --git a/core/sauce.js b/core/sauce.js index 6db85533..0dad1bc9 100644 --- a/core/sauce.js +++ b/core/sauce.js @@ -6,11 +6,11 @@ var iconv = require('iconv-lite'); exports.readSAUCE = readSAUCE; - const SAUCE_SIZE = 128; const SAUCE_ID = new Buffer([0x53, 0x41, 0x55, 0x43, 0x45]); // 'SAUCE' const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' +exports.SAUCE_SIZE = SAUCE_SIZE; // :TODO: SAUCE should be a class // - with getFontName() // - ...other methods @@ -19,6 +19,8 @@ const COMNT_ID = new Buffer([0x43, 0x4f, 0x4d, 0x4e, 0x54]); // 'COMNT' // See // http://www.acid.org/info/sauce/sauce.htm // +const SAUCE_VALID_DATA_TYPES = [0, 1, 2, 3, 4, 5, 6, 7, 8 ]; + function readSAUCE(data, cb) { if(data.length < SAUCE_SIZE) { cb(new Error('No SAUCE record present')); @@ -59,6 +61,11 @@ function readSAUCE(data, cb) { return; } + if(-1 === SAUCE_VALID_DATA_TYPES.indexOf(vars.dataType)) { + cb(new Error('Unsupported SAUCE DataType: ' + vars.dataType)); + return; + } + var sauce = { id : iconv.decode(vars.id, 'cp437'), version : iconv.decode(vars.version, 'cp437').trim(), diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js new file mode 100644 index 00000000..5b2283e5 --- /dev/null +++ b/core/scanner_tossers/ftn_bso.js @@ -0,0 +1,42 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +var MessageScanTossModule = require('../scan_toss_module.js').MessageScanTossModule; +var Config = require('../config.js').config; + +exports.moduleInfo = { + name : 'FTN', + desc : 'FidoNet Style Message Scanner/Tosser', + author : 'NuSkooler', +}; + +exports.getModule = FTNMessageScanTossModule; + +function FTNMessageScanTossModule() { + MessageScanTossModule.call(this); + + this.config = Config.scannerTossers.ftn_bso; + + +} + +require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); + +FTNMessageScanTossModule.prototype.startup = function(cb) { + cb(null); +}; + +FTNMessageScanTossModule.prototype.shutdown = function(cb) { + cb(null); +}; + +FTNMessageScanTossModule.prototype.record = function(message, cb) { + + + cb(null); + + // :TODO: should perhaps record in batches - e.g. start an event, record + // to temp location until time is hit or N achieved such that if multiple + // messages are being created a .FTN file is not made for each one +}; diff --git a/core/theme.js b/core/theme.js index f23c6481..5fd5db75 100644 --- a/core/theme.js +++ b/core/theme.js @@ -523,7 +523,8 @@ function displayThemedPause(options, cb) { if(options.clearPrompt) { if(artInfo.startRow && artInfo.height) { options.client.term.rawWrite(ansi.goto(artInfo.startRow, 1)); - // :TODO: This will not work with NetRunner: + + // Note: Does not work properly in NetRunner < 2.0b17: options.client.term.rawWrite(ansi.deleteLine(artInfo.height)); } else { options.client.term.rawWrite(ansi.eraseLine(1))