* Reworked FTN packet I/O (WIP)

* Detect FTN packet 2, 2.2, and 2+
* Various FTN utils (MSGID, Origin, PID, generation etc)
* More work on message network readyness
This commit is contained in:
Bryan Ashby 2016-02-09 22:30:59 -07:00
parent 317af8419a
commit dec78e942d
11 changed files with 377 additions and 708 deletions

View File

@ -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

View File

@ -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);
},

View File

@ -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')
@ -105,6 +101,30 @@ function FTNPacket() {
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
// Note: The names above match up with object members moment() allows
@ -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);
@ -209,11 +229,14 @@ function FTNPacket() {
// has SEEN-BY, PATH, and other kludge information *appended*
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 : {}, // <KLUDGE> -> [ 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)
});
*/

View File

@ -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):
@ -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;
case 3 :
case '3D' :
addr += '/{0}'.format(address.node);
break;
case 4 :
case '4D':
addr += '.{0}'.format(address.point || 0); // missing and 0 are equiv for point
break;
case 5 :
case '5D' :
if(address.domain) {
addr += '@{0}'.format(address.domain);
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');
}
break;
ftnArea = ftnArea || ''; // AREA is optional
if(!Buffer.isBuffer(ftnArea)) {
ftnArea = iconv.encode(ftnArea, 'CP437');
}
const ns = new Buffer(ENIGMA_FTN_MSGID_NAMESPACE);
let digest = createHash('sha1').update(
Buffer.concat([ ns, ftnMsgId, ftnArea ])).digest();
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: <msgNum>.<conf+area>@<ftnAddr> <serial>
// 2606.agora-agn_tst@46:1/142 19609217
//
// Mystic: <ftnAddress> <serial>
// 46:3/102 46686263
//
// ENiGMA½: <messageId>.<areaTag>@<5dFtnAddress> <serial>
//
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
//
// <unique-part@domain-name>
//
// :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
// *
// Return a FTS-0004 Origin line
// http://ftsc.org/docs/fts-0004.001
//
function getFtnMsgIdKludgeLine(origAddress, messageId) {
if(_.isObject(origAddress)) {
origAddress = getFormattedFTNAddress(origAddress, '5D');
}
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')})`;
}

View File

@ -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 || '';

View File

@ -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);
};

View File

@ -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
};

View File

@ -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);
};

View File

@ -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(),

View File

@ -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
};

View File

@ -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))