diff --git a/core/ftn_address.js b/core/ftn_address.js new file mode 100644 index 00000000..e6dad9e9 --- /dev/null +++ b/core/ftn_address.js @@ -0,0 +1,169 @@ +/* jslint node: true */ +'use strict'; + +let _ = require('lodash'); + +const FTN_ADDRESS_REGEXP = /^([0-9]+:)?([0-9]+)(\/[0-9]+)?(\.[0-9]+)?(@[a-z0-9\-\.]+)?$/i; +const FTN_PATTERN_REGEXP = /^([0-9\*]+:)?([0-9\*]+)(\/[0-9\*]+)?(\.[0-9\*]+)?(@[a-z0-9\-\.\*]+)?$/i; + +module.exports = class Address { + constructor(addr) { + if(addr) { + if(_.isObject(addr)) { + Object.assign(this, addr); + } else if(_.isString(addr)) { + const temp = Address.fromString(addr); + if(temp) { + Object.assign(this, temp); + } + } + } + } + + isEqual(other) { + if(_.isString(other)) { + other = Address.fromString(other); + } + + return ( + this.net === other.net && + this.node === other.node && + this.zone === other.zone && + this.point === other.point && + this.domain === other.domain + ); + } + + isMatch(pattern) { + const m = FTN_PATTERN_REGEXP.exec(pattern); + if(m) { + let addr = { }; + + if(m[1]) { + addr.zone = m[1].slice(0, -1) + if('*' !== addr.zone) { + addr.zone = parseInt(addr.zone); + } + } else { + addr.zone = '*'; + } + + if(m[2]) { + addr.net = m[2]; + if('*' !== addr.net) { + addr.net = parseInt(addr.net); + } + } else { + addr.net = '*'; + } + + if(m[3]) { + addr.node = m[3].substr(1); + if('*' !== addr.node) { + addr.node = parseInt(addr.node); + } + } else { + addr.node = '*'; + } + + if(m[4]) { + addr.point = m[4].substr(1); + if('*' !== addr.point) { + addr.point = parseInt(addr.point); + } + } else { + addr.point = '*'; + } + + if(m[5]) { + addr.domain = m[5].substr(1); + } else { + addr.domain = '*'; + } + + return ( + ('*' === addr.net || this.net === addr.net) && + ('*' === addr.node || this.node === addr.node) && + ('*' === addr.zone || this.zone === addr.zone) && + ('*' === addr.point || this.point === addr.point) && + ('*' === addr.domain || this.domain === addr.domain) + ); + } + + return false; + } + + static fromString(addrStr) { + const m = FTN_ADDRESS_REGEXP.exec(addrStr); + + if(m) { + // start with a 2D + let addr = { + net : parseInt(m[2]), + node : parseInt(m[3].substr(1)), + }; + + // 3D: Addition of zone if present + if(m[1]) { + addr.zone = parseInt(m[1].slice(0, -1)); + } + + // 4D if optional point is present + if(m[4]) { + addr.point = parseInt(m[4].substr(1)); + } + + // 5D with @domain + if(m[5]) { + addr.domain = m[5].substr(1); + } + + return new Address(addr); + } + } + + toString(dimensions) { + dimensions = dimensions || '5D'; + + let addrStr = `${this.zone}:${this.net}`; + + // allow for e.g. '4D' or 5 + const dim = parseInt(dimensions.toString()[0]); + + if(dim >= 3) { + addrStr += `/${this.node}`; + } + + // missing & .0 are equiv for point + if(dim >= 4 && this.point) { + addrStr += `.${this.point}`; + } + + if(5 === dim && this.domain) { + addrStr += `@${this.domain.toLowerCase()}`; + } + + return addrStr; + } + + static getComparator() { + return function(left, right) { + let c = (left.zone || 0) - (right.zone || 0); + if(0 !== c) { + return c; + } + + c = (left.net || 0) - (right.net || 0); + if(0 !== c) { + return c; + } + + c = (left.node || 0) - (right.node || 0); + if(0 !== c) { + return c; + } + + return (left.domain || '').localeCompare(right.domain || ''); + } + } +} diff --git a/core/ftn_mail_packet.js b/core/ftn_mail_packet.js index 361d24dc..51215c5d 100644 --- a/core/ftn_mail_packet.js +++ b/core/ftn_mail_packet.js @@ -2,19 +2,24 @@ 'use strict'; //var MailPacket = require('./mail_packet.js'); -var ftn = require('./ftn_util.js'); -var Message = require('./message.js'); -var sauce = require('./sauce.js'); +let ftn = require('./ftn_util.js'); +let Message = require('./message.js'); +let sauce = require('./sauce.js'); +let Address = require('./ftn_address.js'); +let strUtil = require('./string_util.js'); -var _ = require('lodash'); -var assert = require('assert'); -var binary = require('binary'); -var fs = require('fs'); -var util = require('util'); -var async = require('async'); -var iconv = require('iconv-lite'); -var buffers = require('buffers'); -var moment = require('moment'); +let _ = require('lodash'); +let assert = require('assert'); +let binary = require('binary'); +let fs = require('fs'); +let util = require('util'); +let async = require('async'); +let iconv = require('iconv-lite'); +let buffers = require('buffers'); +let moment = require('moment'); + +exports.PacketHeader = PacketHeader; +exports.Packet = Packet; /* :TODO: things @@ -35,6 +40,88 @@ const FTN_MESSAGE_SAUCE_HEADER = new Buffer('SAUCE00'); const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; +function PacketHeader(options) { +} + +PacketHeader.prototype.init = function(origAddr, destAddr, created, version) { + version = version || '2+'; + + const EMPTY_ADDRESS = { + node : 0, + net : 0, + zone : 0, + point : 0, + }; + + this.packetVersion = version; + + this.setOrigAddress(origAddr || EMPTY_ADDRESS); + this.setDestAddress(destAddr || EMPTY_ADDRESS); + this.setCreated(created || moment()); + + // uncommon to set the following explicitly + this.prodCodeLo = 0xfe; // http://ftsc.org/docs/fta-1005.003 + this.prodRevLo = 0; + this.baud = 0; + this.packetType = FTN_PACKET_HEADER_TYPE; + this.password = ''; + this.prodData = 0x47694e45; // "ENiG" + + this.capWord = 0x0001; + this.capWordValidate = ((this.capWord & 0xff) << 8) | ((this.capWord >> 8) & 0xff); + + return this; +}; + +PacketHeader.prototype.setOrigAddress = function(address) { + this.origNode = address.node; + + // See FSC-48 + if(address.point) { + this.origNet = -1; + this.auxNet = address.net; + } else { + this.origNet = address.net; + this.auxNet = 0; + } + + this.origZone = address.zone; + this.origZone2 = address.zone; + this.origPoint = address.point || 0; + + return this; +}; + +PacketHeader.prototype.setDestAddress = function(address) { + this.destNode = address.node; + this.destNet = address.net; + this.destZone = address.zone; + this.destZone2 = address.zone; + this.destPoint = address.point || 0; + + return this; +}; + +PacketHeader.prototype.setCreated = function(created) { + if(!moment.isMoment(created)) { + created = moment(created); + } + + this.year = created.year(); + this.month = created.month(); + this.day = created.day(); + this.hour = created.hour(); + this.minute = created.minute(); + this.second = created.second(); + + return this; +}; + +PacketHeader.prototype.setPassword = function(password) { + this.password = password.substr(0, 8); +} + + // // Read/Write FTN packets with support for the following formats: // @@ -47,7 +134,7 @@ const FTN_MESSAGE_KLUDGE_PREFIX = '\x01'; // * Writeup on differences between type 2, 2.2, and 2+: // http://walon.org/pub/fidonet/FTSC-nodelists-etc./pkt-types.txt // -function FTNPacket() { +function Packet() { var self = this; @@ -96,7 +183,8 @@ function FTNPacket() { .word32lu('prodData') .tap(packetHeader => { // Convert password from NULL padded array to string - packetHeader.password = ftn.stringFromFTN(packetHeader.password); + //packetHeader.password = ftn.stringFromFTN(packetHeader.password); + packetHeader.password = strUtil.stringFromNullTermBuffer(packetHeader.password, 'CP437'); if(FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) { cb(new Error('Unsupported header type: ' + packetHeader.packetType)); @@ -106,7 +194,7 @@ function FTNPacket() { // // What kind of packet do we really have here? // - // :TODO: adjust values based on version discoverd + // :TODO: adjust values based on version discovered if(FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) { packetHeader.packetVersion = '2.2'; @@ -147,15 +235,16 @@ function FTNPacket() { // packetHeader.created = moment(packetHeader); - cb(null, packetHeader); + let ph = new PacketHeader(); + _.assign(ph, packetHeader); + + cb(null, ph); }); }; this.writePacketHeader = function(packetHeader, ws) { let buffer = new Buffer(FTN_PACKET_HEADER_SIZE); - // :TODO: write 2, 2.2, or 2+ packet based on packetHeader.packetVersion (def=2+) - buffer.writeUInt16LE(packetHeader.origNode, 0); buffer.writeUInt16LE(packetHeader.destNode, 2); buffer.writeUInt16LE(packetHeader.created.year(), 4); @@ -177,7 +266,8 @@ function FTNPacket() { buffer.writeUInt16LE(packetHeader.origZone, 34); buffer.writeUInt16LE(packetHeader.destZone, 36); - switch(packetHeader.packetType) { + // :TODO: update header information appropriately for 2 and 2.2 + switch(packetHeader.packetVersion) { case '2' : // filler... packetHeader.auxNet = 0; @@ -196,9 +286,20 @@ function FTNPacket() { packetHeader.hour = 0; packetHeader.minute = 0; packetHeader.second = 0; + + // :TODO: copy over fields from 2+ -> overriden fields here! + + packetHeader.baud = FTN_PACKET_BAUD_TYPE_2_2; break; case '2+' : + const capWordValidateSwapped = + ((packetHeader.capWordValidate & 0xff) << 8) | + ((packetHeader.capWordValidate >> 8) & 0xff); + + packetHeader.capWordValidate = capWordValidateSwapped; + + // :TODO: set header appropriate if point break; } @@ -355,8 +456,9 @@ function FTNPacket() { .word16lu('ftn_dest_node') .word16lu('ftn_orig_network') .word16lu('ftn_dest_network') - .word8('ftn_attr_flags1') - .word8('ftn_attr_flags2') + .word16lu('ftn_attr_flags') + //.word8('ftn_attr_flags1') + //.word8('ftn_attr_flags2') .word16lu('ftn_cost') .scan('modDateTime', NULL_TERM_BUFFER) // :TODO: 20 bytes max .scan('toUserName', NULL_TERM_BUFFER) // :TODO: 36 bytes max @@ -404,8 +506,9 @@ function FTNPacket() { msg.meta.FtnProperty.ftn_dest_node = msgData.ftn_dest_node; msg.meta.FtnProperty.ftn_orig_network = msgData.ftn_orig_network; msg.meta.FtnProperty.ftn_dest_network = msgData.ftn_dest_network; - msg.meta.FtnProperty.ftn_attr_flags1 = msgData.ftn_attr_flags1; - msg.meta.FtnProperty.ftn_attr_flags2 = msgData.ftn_attr_flags2; + msg.meta.FtnProperty.ftn_attr_flags = msgData.ftn_attr_flags; + //msg.meta.FtnProperty.ftn_attr_flags1 = msgData.ftn_attr_flags1; + //msg.meta.FtnProperty.ftn_attr_flags2 = msgData.ftn_attr_flags2; msg.meta.FtnProperty.ftn_cost = msgData.ftn_cost; self.processMessageBody(msgData.message, function processed(messageBodyData) { @@ -457,8 +560,9 @@ function FTNPacket() { basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_node, 4); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_dest_network, 8); - basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags1, 10); - basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11); + basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10); + //basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags1, 10); + //basicHeader.writeUInt8(message.meta.FtnProperty.ftn_attr_flags2, 11); basicHeader.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12); const dateTimeBuffer = new Buffer(ftn.getDateTimeString(message.modTimestamp) + '\0'); @@ -522,20 +626,25 @@ function FTNPacket() { // // 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); + if(message.meta.FtnProperty.ftn_tear_line) { + msgBody += `${message.meta.FtnProperty.ftn_tear_line}\r`; + } + + // + // Origin line should be near the bottom of a message + // + if(message.meta.FtnProperty.ftn_origin) { + msgBody += `${message.meta.FtnProperty.ftn_origin}\r`; + } // // 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); // note: no ^A (0x01) + appendMeta('\x01PATH', message.meta.FtnKludge['PATH']); ws.write(iconv.encode(msgBody + '\0', 'CP437')); @@ -564,7 +673,31 @@ function FTNPacket() { }; } -FTNPacket.prototype.read = function(pathOrBuffer, iterator, cb) { +// +// Message attributes defined in FTS-0001.016 +// http://ftsc.org/docs/fts-0001.016 +// +Packet.Attribute = { + Private : 0x0001, + Crash : 0x0002, + Received : 0x0004, + Sent : 0x0008, + FileAttached : 0x0010, + InTransit : 0x0020, + Orphan : 0x0040, + KillSent : 0x0080, + Local : 0x0100, + Hold : 0x0200, + Reserved0 : 0x0400, + FileRequest : 0x0800, + ReturnReceiptRequest : 0x1000, + ReturnReceipt : 0x2000, + AuditRequest : 0x4000, + FileUpdateRequest : 0x8000, +}; +Object.freeze(Packet.Attribute); + +Packet.prototype.read = function(pathOrBuffer, iterator, cb) { var self = this; async.series( @@ -591,32 +724,67 @@ FTNPacket.prototype.read = function(pathOrBuffer, iterator, cb) { ); }; -FTNPacket.prototype.write = function(path, packetHeader, messages, cb) { - packetHeader.packetType = packetHeader.packetType || '2+'; - packetHeader.created = packetHeader.created || moment(); - packetHeader.baud = packetHeader.baud || 0; - // :TODO: Other defaults? - - if(!_.isArray(messages)) { - messages = [ messages ] ; +Packet.prototype.writeStream = function(ws, messages, options) { + if(!_.isBoolean(options.terminatePacket)) { + options.terminatePacket = true; + } + + if(_.isObject(options.packetHeader)) { + /* + let packetHeader = options.packetHeader; + + // default header + packetHeader.packetVersion = packetHeader.packetVersion || '2+'; + packetHeader.created = packetHeader.created || moment(); + packetHeader.baud = packetHeader.baud || 0; + */ + this.writePacketHeader(options.packetHeader, ws); } - - let ws = fs.createWriteStream(path); - this.writePacketHeader(packetHeader, ws); messages.forEach(msg => { this.writeMessage(msg, ws); }); - - ws.write(new Buffer( [ 0 ] )); // final extra null term + + if(true === options.terminatePacket) { + ws.write(new Buffer( [ 0 ] )); // final extra null term + } +} + +Packet.prototype.write = function(path, packetHeader, messages) { + if(!_.isArray(messages)) { + messages = [ messages ]; + } + + this.writeStream( + fs.createWriteStream(path), // :TODO: specify mode/etc. + messages, + { packetHeader : packetHeader, terminatePacket : true } + ); }; -var ftnPacket = new FTNPacket(); +const LOCAL_ADDRESS = { + zone : 46, + net : 1, + node : 232, + domain : 'l33t.codes', +}; + +const REMOTE_ADDRESS = { + zone : 1, + net : 2, + node : 218, +}; + +var packetHeader1 = new PacketHeader(); +packetHeader1.init(LOCAL_ADDRESS, REMOTE_ADDRESS); +console.log(packetHeader1); + +var packet = new Packet(); var theHeader; var written = false; let messagesToWrite = []; -ftnPacket.read( +packet.read( process.argv[2], function iterator(dataType, data) { if('header' === dataType) { @@ -633,7 +801,7 @@ ftnPacket.read( written = true; let messages = [ msg ]; - ftnPacket.write('/home/nuskooler/Downloads/ftnout/test1.pkt', theHeader, messages, err => { + Packet.write('/home/nuskooler/Downloads/ftnout/test1.pkt', theHeader, messages, err => { }); @@ -650,8 +818,13 @@ ftnPacket.read( 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.parseAddress('46:1/232.4@l33t.codes')) + console.log(Address.fromString('46:1/232.4@l33t.codes')); console.log(ftn.getUTCTimeZoneOffset()) + console.log(ftn.getUpdatedSeenByEntries( + msg.meta.FtnProperty.ftn_seen_by, '1/107 4/22 4/25 4/10')); + console.log(ftn.getUpdatedPathEntries( + msg.meta.FtnKludge['PATH'], '1:365/50')) } }, function completion(err) { @@ -660,7 +833,7 @@ ftnPacket.read( console.log(messagesToWrite.length) - ftnPacket.write('/home/nuskooler/Downloads/ftnout/test1.pkt', theHeader, messagesToWrite, err => { + packet.write('/home/nuskooler/Downloads/ftnout/test1.pkt', theHeader, messagesToWrite, err => { }); } ); diff --git a/core/ftn_util.js b/core/ftn_util.js index 94f716f2..a5cec2a2 100644 --- a/core/ftn_util.js +++ b/core/ftn_util.js @@ -2,7 +2,7 @@ 'use strict'; var Config = require('./config.js').config; - +var Address = require('./ftn_address.js'); var _ = require('lodash'); var assert = require('assert'); @@ -18,20 +18,22 @@ 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.createMessageUuid = createMessageUuid; -exports.parseAddress = parseAddress; -exports.formatAddress = formatAddress; -exports.getDateFromFtnDateTime = getDateFromFtnDateTime; -exports.getDateTimeString = getDateTimeString; +exports.getDateFromFtnDateTime = getDateFromFtnDateTime; +exports.getDateTimeString = getDateTimeString; -exports.getMessageIdentifier = getMessageIdentifier; -exports.getProductIdentifier = getProductIdentifier; -exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; -exports.getOrigin = getOrigin; +exports.getMessageIdentifier = getMessageIdentifier; +exports.getProductIdentifier = getProductIdentifier; +exports.getUTCTimeZoneOffset = getUTCTimeZoneOffset; +exports.getOrigin = getOrigin; +exports.getTearLine = getTearLine; +exports.getAbbreviatedNetNodeList = getAbbreviatedNetNodeList; +exports.parseAbbreviatedNetNodeList = parseAbbreviatedNetNodeList; +exports.getUpdatedSeenByEntries = getUpdatedSeenByEntries; +exports.getUpdatedPathEntries = getUpdatedPathEntries; -exports.getQuotePrefix = getQuotePrefix; +exports.getQuotePrefix = getQuotePrefix; // // Namespace for RFC-4122 name based UUIDs generated from @@ -40,23 +42,10 @@ exports.getQuotePrefix = getQuotePrefix; 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; +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 -function stringFromFTN(buf, encoding) { - var nullPos = buf.length; - for(var i = 0; i < buf.length; ++i) { - if(0x00 === buf[i]) { - nullPos = i; - break; - } - } - - return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8'); -} - function stringToNullPaddedBuffer(s, bufLen) { let buffer = new Buffer(bufLen).fill(0x00); let enc = iconv.encode(s, 'CP437').slice(0, bufLen); @@ -143,57 +132,6 @@ function createMessageUuid(ftnMsgId, ftnArea) { 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 getMessageSerialNumber(message) { return ('00000000' + ((Math.floor((Date.now() - Date.UTC(2016, 1, 1)) / 1000) + message.messageId)).toString(16)).substr(-8); @@ -235,7 +173,8 @@ function getMessageSerialNumber(message) { // ENiGMA½: .@<5dFtnAddress> // function getMessageIdentifier(message, address) { - return `${message.messageId}.${message.areaTag.toLowerCase()}@${formatAddress(address, '5D')} ${getMessageSerialNumber(message)}`; + const addrStr = new Address(address).toString('5D'); + return `${message.messageId}.${message.areaTag.toLowerCase()}@${addrStr} ${getMessageSerialNumber(message)}`; } // @@ -288,5 +227,113 @@ function getOrigin(address) { Config.messageNetworks.originName : Config.general.boardName; - return ` * Origin: ${origin} (${formatAddress(address, '5D')})`; + const addrStr = new Address(address).toString('5D'); + return ` * Origin: ${origin} (${addrStr})`; +} + +function getTearLine() { + return `--- ENiGMA 1/2 v{$packageJson.version} (${os.platform()}; ${os.arch()}; ${nodeVer})`; +} + +function getAbbreviatedNetNodeList(netNodes) { + let abbrList = ''; + let currNet; + netNodes.forEach(netNode => { + if(currNet !== netNode.net) { + abbrList += `${netNode.net}/`; + currNet = netNode.net; + } + abbrList += `${netNode.node} `; + }); + + return abbrList.trim(); // remove trailing space +} + +function parseAbbreviatedNetNodeList(netNodes) { + // + // Make sure we have an array of objects. + // Allow for a single object or string(s) + // + if(!_.isArray(netNodes)) { + if(_.isString(netNodes)) { + netNodes = netNodes.split(' '); + } else { + netNodes = [ netNodes ]; + } + } + + // + // Convert any strings to parsed address objects + // + return netNodes.map(a => { + if(_.isObject(a)) { + return a; + } else { + return Address.fromString(a); + } + }); +} + +// +// Return a FTS-0004.001 SEEN-BY entry(s) that include +// all pre-existing SEEN-BY entries with the addition +// of |additions|. +// +// See http://ftsc.org/docs/fts-0004.001 +// and notes at http://ftsc.org/docs/fsc-0043.002. +// +// For a great write up, see http://www.skepticfiles.org/aj/basics03.htm +// +// This method returns an sorted array of values, but +// not the "SEEN-BY" prefix itself +// +function getUpdatedSeenByEntries(existingEntries, additions) { + /* + From FTS-0004: + + "There can be many seen-by lines at the end of Conference + Mail messages, and they are the real "meat" of the control + information. They are used to determine the systems to + receive the exported messages. The format of the line is: + + SEEN-BY: 132/101 113 136/601 1014/1 + + The net/node numbers correspond to the net/node numbers of + the systems having already received the message. In this way + a message is never sent to a system twice. In a conference + with many participants the number of seen-by lines can be + very large. This line is added if it is not already a part + of the message, or added to if it already exists, each time + a message is exported to other systems. This is a REQUIRED + field, and Conference Mail will not function correctly if + this field is not put in place by other Echomail compatible + programs." + */ + existingEntries = existingEntries || []; + if(!_.isArray(existingEntries)) { + existingEntries = [ existingEntries ]; + } + + additions = parseAbbreviatedNetNodeList(additions).sort(Address.getComparator()); + + // + // For now, we'll just append a new SEEN-BY entry + // + // :TODO: we should at least try and update what is already there in a smart way + existingEntries.push(getAbbreviatedNetNodeList(additions)); + return existingEntries; +} + +function getUpdatedPathEntries(existingEntries, localAddress) { + // :TODO: append to PATH in a smart way! We shoudl try to fit at least the last existing line + + existingEntries = existingEntries || []; + if(!_.isArray(existingEntries)) { + existingEntries = [ existingEntries ]; + } + + existingEntries.push(getAbbreviatedNetNodeList( + parseAbbreviatedNetNodeList(localAddress))); + + return existingEntries; } diff --git a/core/message.js b/core/message.js index 4e0bd0a9..f506f91b 100644 --- a/core/message.js +++ b/core/message.js @@ -109,8 +109,9 @@ Message.FtnPropertyNames = { FtnDestNode : 'ftn_dest_node', FtnOrigNetwork : 'ftn_orig_network', FtnDestNetwork : 'ftn_dest_network', - FtnAttrFlags1 : 'ftn_attr_flags1', - FtnAttrFlags2 : 'ftn_attr_flags2', + FtnAttrFlags : 'ftn_attr_flags', + //FtnAttrFlags1 : 'ftn_attr_flags1', + //FtnAttrFlags2 : 'ftn_attr_flags2', FtnCost : 'ftn_cost', FtnOrigZone : 'ftn_orig_zone', FtnDestZone : 'ftn_dest_zone', diff --git a/core/scanner_tossers/ftn_bso.js b/core/scanner_tossers/ftn_bso.js index 5b2283e5..57b6f94e 100644 --- a/core/scanner_tossers/ftn_bso.js +++ b/core/scanner_tossers/ftn_bso.js @@ -2,8 +2,12 @@ 'use strict'; // ENiGMA½ -var MessageScanTossModule = require('../scan_toss_module.js').MessageScanTossModule; -var Config = require('../config.js').config; +let MessageScanTossModule = require('../scan_toss_module.js').MessageScanTossModule; +let Config = require('../config.js').config; +let ftnMailpacket = require('../ftn_mail_packet.js'); +let ftnUtil = require('../ftn_util.js'); + +let moment = require('moment'); exports.moduleInfo = { name : 'FTN', @@ -18,7 +22,60 @@ function FTNMessageScanTossModule() { this.config = Config.scannerTossers.ftn_bso; - + this.createMessagePacket = function(message, config) { + this.prepareMessage(message); + + let packet = new ftnMailPacket.Packet(); + + let packetHeader = new ftnMailpacket.PacketHeader(); + packetHeader.init( + config.network.localAddress, + config.remoteAddress); + + packetHeader.setPassword(config.remoteNode.packetPassword || ''); + }; + + this.prepareMessage = function(message, config) { + // + // Set various FTN kludges/etc. + // + message.meta.FtnProperty = message.meta.FtnProperty || {}; + message.meta.FtnProperty.ftn_orig_node = config.network.localAddress.node; + message.meta.FtnProperty.ftn_dest_node = config.remoteAddress.node; + message.meta.FtnProperty.ftn_orig_network = config.network.localAddress.net; + message.meta.FtnProperty.ftn_dest_network = config.remoteAddress.net; + // :TODO: attr1 & 2 + message.meta.FtnProperty.ftn_cost = 0; + + message.meta.FtnProperty.ftn_tear_line = ftnUtil.getTearLine(); + message.meta.FtnProperty.ftn_origin = ftnUtil.getOrigin(config.network.localAddress); + + if(message.areaTag) { + message.meta.FtnProperty.ftn_area = message.areaTag; + } else { + // :TODO: add "Via" line -- FSP-1010 + } + + // + // When exporting messages, we should create/update SEEN-BY + // with remote address(s) we are exporting to. + // + message.meta.FtnProperty.ftn_seen_by = + ftnUtil.getUpdatedSeenByEntries( + message.meta.FtnProperty.ftn_seen_by, + Config.messageNetworks.ftn.areas[message.areaTag].uplinks + ); + + // + // And create/update PATH for ourself + // + message.meta.FtnKludge['PATH'] = + ftnUtil.getUpdatedPathEntries( + message.meta.FtnKludge['PATH'], + config.network.localAddress.node + ); + }; + } require('util').inherits(FTNMessageScanTossModule, MessageScanTossModule); diff --git a/core/string_util.js b/core/string_util.js index f98d0d5a..02c28435 100644 --- a/core/string_util.js +++ b/core/string_util.js @@ -1,14 +1,16 @@ /* jslint node: true */ 'use strict'; -var miscUtil = require('./misc_util.js'); +let miscUtil = require('./misc_util.js'); +let iconv = require('iconv-lite'); -exports.stylizeString = stylizeString; -exports.pad = pad; -exports.replaceAt = replaceAt; -exports.isPrintable = isPrintable; -exports.debugEscapedString = debugEscapedString; +exports.stylizeString = stylizeString; +exports.pad = pad; +exports.replaceAt = replaceAt; +exports.isPrintable = isPrintable; +exports.debugEscapedString = debugEscapedString; +exports.stringFromNullTermBuffer = stringFromNullTermBuffer; // :TODO: create Unicode verison of this var VOWELS = [ 'a', 'e', 'i', 'o', 'u' ]; @@ -176,6 +178,23 @@ function debugEscapedString(s) { return JSON.stringify(s).slice(1, -1); } +function stringFromNullTermBuffer(buf, encoding) { + /*var nullPos = buf.length; + for(var i = 0; i < buf.length; ++i) { + if(0x00 === buf[i]) { + nullPos = i; + break; + } + } + */ + let nullPos = buf.indexOf(new Buffer( [ 0x00 ] )); + if(-1 === nullPos) { + nullPos = buf.length; + } + + return iconv.decode(buf.slice(0, nullPos), encoding || 'utf-8'); +} + // // Extend String.format's object syntax with some modifiers // e.g.: '{username!styleL33t}'.format( { username : 'Leet User' } ) -> "L33t U53r"