1280 lines
46 KiB
JavaScript
1280 lines
46 KiB
JavaScript
/* jslint node: true */
|
|
'use strict';
|
|
|
|
const ftn = require('./ftn_util.js');
|
|
const Message = require('./message.js');
|
|
const sauce = require('./sauce.js');
|
|
const Address = require('./ftn_address.js');
|
|
const strUtil = require('./string_util.js');
|
|
const Log = require('./logger.js').log;
|
|
const ansiPrep = require('./ansi_prep.js');
|
|
const Errors = require('./enig_error.js').Errors;
|
|
|
|
const _ = require('lodash');
|
|
const assert = require('assert');
|
|
const { Parser } = require('binary-parser');
|
|
const fs = require('graceful-fs');
|
|
const async = require('async');
|
|
const iconv = require('iconv-lite');
|
|
const moment = require('moment');
|
|
|
|
exports.Packet = Packet;
|
|
|
|
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;
|
|
|
|
// SAUCE magic header + version ("00")
|
|
const FTN_MESSAGE_SAUCE_HEADER = Buffer.from('SAUCE00');
|
|
|
|
const FTN_MESSAGE_KLUDGE_PREFIX = '\x01';
|
|
|
|
class PacketHeader {
|
|
constructor(origAddr, destAddr, version, createdMoment) {
|
|
const EMPTY_ADDRESS = {
|
|
node: 0,
|
|
net: 0,
|
|
zone: 0,
|
|
point: 0,
|
|
};
|
|
|
|
this.version = version || '2+';
|
|
this.origAddress = origAddr || EMPTY_ADDRESS;
|
|
this.destAddress = destAddr || EMPTY_ADDRESS;
|
|
this.created = createdMoment || 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); // swap
|
|
|
|
this.prodCodeHi = 0xfe; // see above
|
|
this.prodRevHi = 0;
|
|
}
|
|
|
|
get origAddress() {
|
|
let addr = new Address({
|
|
node: this.origNode,
|
|
zone: this.origZone,
|
|
});
|
|
|
|
if (this.origPoint) {
|
|
addr.point = this.origPoint;
|
|
addr.net = this.auxNet;
|
|
} else {
|
|
addr.net = this.origNet;
|
|
}
|
|
|
|
return addr;
|
|
}
|
|
|
|
set origAddress(address) {
|
|
if (_.isString(address)) {
|
|
address = Address.fromString(address);
|
|
}
|
|
|
|
this.origNode = address.node;
|
|
|
|
// See FSC-48
|
|
// :TODO: disabled for now until we have separate packet writers for 2, 2+, 2+48, and 2.2
|
|
/*if(address.point) {
|
|
this.auxNet = address.origNet;
|
|
this.origNet = -1;
|
|
} else {
|
|
this.origNet = address.net;
|
|
this.auxNet = 0;
|
|
}
|
|
*/
|
|
this.origNet = address.net;
|
|
this.auxNet = 0;
|
|
|
|
this.origZone = address.zone;
|
|
this.origZone2 = address.zone;
|
|
this.origPoint = address.point || 0;
|
|
}
|
|
|
|
get destAddress() {
|
|
let addr = new Address({
|
|
node: this.destNode,
|
|
net: this.destNet,
|
|
zone: this.destZone,
|
|
});
|
|
|
|
if (this.destPoint) {
|
|
addr.point = this.destPoint;
|
|
}
|
|
|
|
return addr;
|
|
}
|
|
|
|
set destAddress(address) {
|
|
if (_.isString(address)) {
|
|
address = Address.fromString(address);
|
|
}
|
|
|
|
this.destNode = address.node;
|
|
this.destNet = address.net;
|
|
this.destZone = address.zone;
|
|
this.destZone2 = address.zone;
|
|
this.destPoint = address.point || 0;
|
|
}
|
|
|
|
get created() {
|
|
return moment({
|
|
year: this.year,
|
|
month: this.month - 1, // moment uses 0 indexed months
|
|
date: this.day,
|
|
hour: this.hour,
|
|
minute: this.minute,
|
|
second: this.second,
|
|
});
|
|
}
|
|
|
|
set created(momentCreated) {
|
|
if (!moment.isMoment(momentCreated)) {
|
|
momentCreated = moment(momentCreated);
|
|
}
|
|
|
|
this.year = momentCreated.year();
|
|
this.month = momentCreated.month() + 1; // moment uses 0 indexed months
|
|
this.day = momentCreated.date(); // day of month
|
|
this.hour = momentCreated.hour();
|
|
this.minute = momentCreated.minute();
|
|
this.second = momentCreated.second();
|
|
}
|
|
}
|
|
|
|
exports.PacketHeader = PacketHeader;
|
|
|
|
//
|
|
// Read/Write FTN packets with support for the following formats:
|
|
//
|
|
// * Type 2 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
|
|
//
|
|
const PacketHeaderParser = new Parser()
|
|
.uint16le('origNode')
|
|
.uint16le('destNode')
|
|
.uint16le('year')
|
|
.uint16le('month')
|
|
.uint16le('day')
|
|
.uint16le('hour')
|
|
.uint16le('minute')
|
|
.uint16le('second')
|
|
.uint16le('baud')
|
|
.uint16le('packetType')
|
|
.uint16le('origNet')
|
|
.uint16le('destNet')
|
|
.int8('prodCodeLo')
|
|
.int8('prodRevLo') // aka serialNo
|
|
.buffer('password', { length: 8 }) // can't use string; need CP437 - see https://github.com/keichi/binary-parser/issues/33
|
|
.uint16le('origZone')
|
|
.uint16le('destZone')
|
|
//
|
|
// The following is "filler" in FTS-0001, specifics in
|
|
// FSC-0045 and FSC-0048
|
|
//
|
|
.uint16le('auxNet')
|
|
.uint16le('capWordValidate')
|
|
.int8('prodCodeHi')
|
|
.int8('prodRevHi')
|
|
.uint16le('capWord')
|
|
.uint16le('origZone2')
|
|
.uint16le('destZone2')
|
|
.uint16le('origPoint')
|
|
.uint16le('destPoint')
|
|
.uint32le('prodData');
|
|
|
|
const MessageHeaderParser = new Parser()
|
|
.uint16le('messageType')
|
|
.uint16le('ftn_msg_orig_node')
|
|
.uint16le('ftn_msg_dest_node')
|
|
.uint16le('ftn_msg_orig_net')
|
|
.uint16le('ftn_msg_dest_net')
|
|
.uint16le('ftn_attr_flags')
|
|
.uint16le('ftn_cost')
|
|
//
|
|
// It would be nice to just string() these, but we want CP437 which requires
|
|
// iconv. Another option would be to use a formatter, but until issue 33
|
|
// (https://github.com/keichi/binary-parser/issues/33) is fixed, this is cumbersome.
|
|
//
|
|
.array('modDateTime', {
|
|
type: 'uint8',
|
|
length: 20, // FTS-0001.016: 20 bytes
|
|
})
|
|
.array('toUserName', {
|
|
type: 'uint8',
|
|
// :TODO: array needs some soft of 'limit' field
|
|
readUntil: b => 0x00 === b,
|
|
})
|
|
.array('fromUserName', {
|
|
type: 'uint8',
|
|
readUntil: b => 0x00 === b,
|
|
})
|
|
.array('subject', {
|
|
type: 'uint8',
|
|
readUntil: b => 0x00 === b,
|
|
})
|
|
.array('message', {
|
|
type: 'uint8',
|
|
readUntil: b => 0x00 === b,
|
|
});
|
|
|
|
function Packet(options) {
|
|
var self = this;
|
|
|
|
this.options = options || {};
|
|
|
|
this.parsePacketHeader = function (packetBuffer, cb) {
|
|
assert(Buffer.isBuffer(packetBuffer));
|
|
|
|
let packetHeader;
|
|
try {
|
|
packetHeader = PacketHeaderParser.parse(packetBuffer);
|
|
} catch (e) {
|
|
return Errors.Invalid(`Unable to parse FTN packet header: ${e.message}`);
|
|
}
|
|
|
|
// Convert password from NULL padded array to string
|
|
packetHeader.password = strUtil.stringFromNullTermBuffer(
|
|
packetHeader.password,
|
|
'CP437'
|
|
);
|
|
|
|
if (FTN_PACKET_HEADER_TYPE !== packetHeader.packetType) {
|
|
return cb(
|
|
Errors.Invalid(
|
|
`Unsupported FTN packet header type: ${packetHeader.packetType}`
|
|
)
|
|
);
|
|
}
|
|
|
|
//
|
|
// What kind of packet do we really have here?
|
|
//
|
|
// :TODO: adjust values based on version discovered
|
|
if (FTN_PACKET_BAUD_TYPE_2_2 === packetHeader.baud) {
|
|
packetHeader.version = '2.2';
|
|
|
|
// See FSC-0045
|
|
packetHeader.origPoint = packetHeader.year;
|
|
packetHeader.destPoint = packetHeader.month;
|
|
|
|
packetHeader.destDomain = packetHeader.origZone2;
|
|
packetHeader.origDomain = packetHeader.auxNet;
|
|
} else {
|
|
//
|
|
// See heuristics described in FSC-0048, "Receiving Type-2+ bundles"
|
|
//
|
|
const capWordValidateSwapped =
|
|
((packetHeader.capWordValidate & 0xff) << 8) |
|
|
((packetHeader.capWordValidate >> 8) & 0xff);
|
|
|
|
if (
|
|
capWordValidateSwapped === packetHeader.capWord &&
|
|
0 != packetHeader.capWord &&
|
|
packetHeader.capWord & 0x0001
|
|
) {
|
|
packetHeader.version = '2+';
|
|
|
|
// See FSC-0048
|
|
if (-1 === packetHeader.origNet) {
|
|
packetHeader.origNet = packetHeader.auxNet;
|
|
}
|
|
} else {
|
|
packetHeader.version = '2';
|
|
|
|
// :TODO: should fill bytes be 0?
|
|
}
|
|
}
|
|
|
|
packetHeader.created = moment({
|
|
year: packetHeader.year,
|
|
month: packetHeader.month - 1, // moment uses 0 indexed months
|
|
date: packetHeader.day,
|
|
hour: packetHeader.hour,
|
|
minute: packetHeader.minute,
|
|
second: packetHeader.second,
|
|
});
|
|
|
|
const ph = new PacketHeader();
|
|
_.assign(ph, packetHeader);
|
|
|
|
return cb(null, ph);
|
|
};
|
|
|
|
this.getPacketHeaderBuffer = function (packetHeader) {
|
|
let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE);
|
|
|
|
buffer.writeUInt16LE(packetHeader.origNode, 0);
|
|
buffer.writeUInt16LE(packetHeader.destNode, 2);
|
|
buffer.writeUInt16LE(packetHeader.year, 4);
|
|
buffer.writeUInt16LE(packetHeader.month, 6);
|
|
buffer.writeUInt16LE(packetHeader.day, 8);
|
|
buffer.writeUInt16LE(packetHeader.hour, 10);
|
|
buffer.writeUInt16LE(packetHeader.minute, 12);
|
|
buffer.writeUInt16LE(packetHeader.second, 14);
|
|
|
|
buffer.writeUInt16LE(packetHeader.baud, 16);
|
|
buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18);
|
|
buffer.writeUInt16LE(
|
|
-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet,
|
|
20
|
|
);
|
|
buffer.writeUInt16LE(packetHeader.destNet, 22);
|
|
buffer.writeUInt8(packetHeader.prodCodeLo, 24);
|
|
buffer.writeUInt8(packetHeader.prodRevHi, 25);
|
|
|
|
const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8);
|
|
pass.copy(buffer, 26);
|
|
|
|
buffer.writeUInt16LE(packetHeader.origZone, 34);
|
|
buffer.writeUInt16LE(packetHeader.destZone, 36);
|
|
buffer.writeUInt16LE(packetHeader.auxNet, 38);
|
|
buffer.writeUInt16LE(packetHeader.capWordValidate, 40);
|
|
buffer.writeUInt8(packetHeader.prodCodeHi, 42);
|
|
buffer.writeUInt8(packetHeader.prodRevLo, 43);
|
|
buffer.writeUInt16LE(packetHeader.capWord, 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;
|
|
};
|
|
|
|
this.writePacketHeader = function (packetHeader, ws) {
|
|
let buffer = Buffer.alloc(FTN_PACKET_HEADER_SIZE);
|
|
|
|
buffer.writeUInt16LE(packetHeader.origNode, 0);
|
|
buffer.writeUInt16LE(packetHeader.destNode, 2);
|
|
buffer.writeUInt16LE(packetHeader.year, 4);
|
|
buffer.writeUInt16LE(packetHeader.month, 6);
|
|
buffer.writeUInt16LE(packetHeader.day, 8);
|
|
buffer.writeUInt16LE(packetHeader.hour, 10);
|
|
buffer.writeUInt16LE(packetHeader.minute, 12);
|
|
buffer.writeUInt16LE(packetHeader.second, 14);
|
|
|
|
buffer.writeUInt16LE(packetHeader.baud, 16);
|
|
buffer.writeUInt16LE(FTN_PACKET_HEADER_TYPE, 18);
|
|
buffer.writeUInt16LE(
|
|
-1 === packetHeader.origNet ? 0xffff : packetHeader.origNet,
|
|
20
|
|
);
|
|
buffer.writeUInt16LE(packetHeader.destNet, 22);
|
|
buffer.writeUInt8(packetHeader.prodCodeLo, 24);
|
|
buffer.writeUInt8(packetHeader.prodRevHi, 25);
|
|
|
|
const pass = ftn.stringToNullPaddedBuffer(packetHeader.password, 8);
|
|
pass.copy(buffer, 26);
|
|
|
|
buffer.writeUInt16LE(packetHeader.origZone, 34);
|
|
buffer.writeUInt16LE(packetHeader.destZone, 36);
|
|
buffer.writeUInt16LE(packetHeader.auxNet, 38);
|
|
buffer.writeUInt16LE(packetHeader.capWordValidate, 40);
|
|
buffer.writeUInt8(packetHeader.prodCodeHi, 42);
|
|
buffer.writeUInt8(packetHeader.prodRevLo, 43);
|
|
buffer.writeUInt16LE(packetHeader.capWord, 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);
|
|
|
|
ws.write(buffer);
|
|
|
|
return buffer.length;
|
|
};
|
|
|
|
this.processMessageBody = function (messageBodyBuffer, 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 can be a bit tricky:
|
|
// * Decoding as CP437 converts 0x8d -> 0xec, so we'll need to correct for that
|
|
// * Many kludge lines specify an encoding. If we find one of such lines, we'll
|
|
// likely need to re-decode as the specified encoding
|
|
// * SAUCE is binary-ish data, so we need to inspect for it before any
|
|
// decoding occurs
|
|
//
|
|
let messageBodyData = {
|
|
message: [],
|
|
kludgeLines: {}, // KLUDGE:[value1, value2, ...] map
|
|
seenBy: [],
|
|
};
|
|
|
|
function addKludgeLine(line) {
|
|
//
|
|
// We have to special case INTL/TOPT/FMPT as they don't contain
|
|
// a ':' name/value separator like the rest of the kludge lines... because stupdity.
|
|
//
|
|
let key = line.substr(0, 4).trim();
|
|
let value;
|
|
if (['INTL', 'TOPT', 'FMPT', 'Via'].includes(key)) {
|
|
value = line.substr(key.length).trim();
|
|
} else {
|
|
const sepIndex = line.indexOf(':');
|
|
key = line.substr(0, sepIndex).toUpperCase();
|
|
value = line.substr(sepIndex + 1).trim();
|
|
}
|
|
|
|
//
|
|
// Allow mapped value to be either a key:value if there is only
|
|
// one entry, or key:[value1, value2,...] if there are more
|
|
//
|
|
if (messageBodyData.kludgeLines[key]) {
|
|
if (!_.isArray(messageBodyData.kludgeLines[key])) {
|
|
messageBodyData.kludgeLines[key] = [messageBodyData.kludgeLines[key]];
|
|
}
|
|
messageBodyData.kludgeLines[key].push(value);
|
|
} else {
|
|
messageBodyData.kludgeLines[key] = value;
|
|
}
|
|
}
|
|
|
|
let encoding = 'cp437';
|
|
|
|
async.series(
|
|
[
|
|
function extractSauce(callback) {
|
|
// :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
|
|
);
|
|
if (sauceHeaderPosition > -1) {
|
|
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.slice(
|
|
sauceHeaderPosition + sauce.SAUCE_SIZE
|
|
);
|
|
// messageBodyBuffer = messageBodyBuffer.slice(0, sauceHeaderPosition);
|
|
messageBodyData.sauce = theSauce;
|
|
} else {
|
|
Log.warn(
|
|
{ error: err.message },
|
|
'Found what looks like to be a SAUCE record, but failed to read'
|
|
);
|
|
}
|
|
return callback(null); // failure to read SAUCE is OK
|
|
}
|
|
);
|
|
} else {
|
|
callback(null);
|
|
}
|
|
},
|
|
function extractChrsAndDetermineEncoding(callback) {
|
|
//
|
|
// From FTS-5003.001:
|
|
// "The CHRS control line is formatted as follows:
|
|
//
|
|
// ^ACHRS: <identifier> <level>
|
|
//
|
|
// Where <identifier> is a character string of no more than eight (8)
|
|
// ASCII characters identifying the character set or character encoding
|
|
// scheme used, and level is a positive integer value describing what
|
|
// level of CHRS the message is written in."
|
|
//
|
|
// Also according to the spec, the deprecated "CHARSET" value may be used
|
|
// :TODO: Look into CHARSET more - should we bother supporting it?
|
|
// :TODO: See encodingFromHeader() for CHRS/CHARSET support @ https://github.com/Mithgol/node-fidonet-jam
|
|
const FTN_CHRS_PREFIX = Buffer.from([
|
|
0x01, 0x43, 0x48, 0x52, 0x53, 0x3a, 0x20,
|
|
]); // "\x01CHRS:"
|
|
const FTN_CHRS_SUFFIX = Buffer.from([0x0d]);
|
|
|
|
let chrsPrefixIndex = messageBodyBuffer.indexOf(FTN_CHRS_PREFIX);
|
|
if (chrsPrefixIndex < 0) {
|
|
return callback(null);
|
|
}
|
|
|
|
chrsPrefixIndex += FTN_CHRS_PREFIX.length;
|
|
|
|
const chrsEndIndex = messageBodyBuffer.indexOf(
|
|
FTN_CHRS_SUFFIX,
|
|
chrsPrefixIndex
|
|
);
|
|
if (chrsEndIndex < 0) {
|
|
return callback(null);
|
|
}
|
|
|
|
let chrsContent = messageBodyBuffer.slice(
|
|
chrsPrefixIndex,
|
|
chrsEndIndex
|
|
);
|
|
if (0 === chrsContent.length) {
|
|
return callback(null);
|
|
}
|
|
|
|
chrsContent = iconv.decode(chrsContent, 'CP437');
|
|
const chrsEncoding =
|
|
ftn.getEncodingFromCharacterSetIdentifier(chrsContent);
|
|
if (chrsEncoding) {
|
|
encoding = chrsEncoding;
|
|
}
|
|
return callback(null);
|
|
},
|
|
function extractMessageData(callback) {
|
|
//
|
|
// Decode |messageBodyBuffer| using |encoding| defaulted or detected above
|
|
//
|
|
// :TODO: Look into \xec thing more - document
|
|
let decoded;
|
|
try {
|
|
decoded = iconv.decode(messageBodyBuffer, encoding);
|
|
} catch (e) {
|
|
Log.debug(
|
|
{ encoding: encoding, error: e.toString() },
|
|
'Error decoding. Falling back to ASCII'
|
|
);
|
|
decoded = iconv.decode(messageBodyBuffer, 'ascii');
|
|
}
|
|
|
|
const messageLines = strUtil.splitTextAtTerms(
|
|
decoded.replace(/\xec/g, '')
|
|
);
|
|
let endOfMessage = false;
|
|
|
|
messageLines.forEach(line => {
|
|
if (0 === line.length) {
|
|
messageBodyData.message.push('');
|
|
return;
|
|
}
|
|
|
|
if (line.startsWith('AREA:')) {
|
|
messageBodyData.area = line
|
|
.substring(line.indexOf(':') + 1)
|
|
.trim();
|
|
} else if (line.startsWith('--- ')) {
|
|
// Tear Lines are tracked allowing for specialized display/etc.
|
|
messageBodyData.tearLine = line;
|
|
} else if (/^[ ]{1,2}\* Origin: /.test(line)) {
|
|
// To spec is " * Origin: ..."
|
|
messageBodyData.originLine = line;
|
|
endOfMessage = true; // Anything past origin is not part of the message body
|
|
} else if (line.startsWith('SEEN-BY:')) {
|
|
endOfMessage = true; // Anything past the first SEEN-BY is not part of the message body
|
|
messageBodyData.seenBy.push(
|
|
line.substring(line.indexOf(':') + 1).trim()
|
|
);
|
|
} else if (FTN_MESSAGE_KLUDGE_PREFIX === line.charAt(0)) {
|
|
if ('PATH:' === line.slice(1, 6)) {
|
|
endOfMessage = true; // Anything pats the first PATH is not part of the message body
|
|
}
|
|
addKludgeLine(line.slice(1));
|
|
} else if (!endOfMessage) {
|
|
// regular ol' message line
|
|
messageBodyData.message.push(line);
|
|
}
|
|
});
|
|
|
|
return callback(null);
|
|
},
|
|
],
|
|
() => {
|
|
messageBodyData.message = messageBodyData.message.join('\n');
|
|
return cb(messageBodyData);
|
|
}
|
|
);
|
|
};
|
|
|
|
this.parsePacketMessages = function (header, packetBuffer, iterator, cb) {
|
|
//
|
|
// Check for end-of-messages marker up front before parse so we can easily
|
|
// tell the difference between end and bad header
|
|
//
|
|
if (packetBuffer.length < 3) {
|
|
const peek = packetBuffer.slice(0, 2);
|
|
if (
|
|
peek.equals(Buffer.from([0x00])) ||
|
|
peek.equals(Buffer.from([0x00, 0x00]))
|
|
) {
|
|
// end marker - no more messages
|
|
return cb(null);
|
|
}
|
|
// else fall through & hit exception below to log error
|
|
}
|
|
|
|
let msgData;
|
|
try {
|
|
msgData = MessageHeaderParser.parse(packetBuffer);
|
|
} catch (e) {
|
|
return cb(Errors.Invalid(`Failed to parse FTN message header: ${e.message}`));
|
|
}
|
|
|
|
if (FTN_PACKET_MESSAGE_TYPE != msgData.messageType) {
|
|
return cb(
|
|
Errors.Invalid(`Unsupported FTN message type: ${msgData.messageType}`)
|
|
);
|
|
}
|
|
|
|
//
|
|
// Convert null terminated arrays to strings
|
|
//
|
|
// From FTS-0001.016:
|
|
// * modDateTime: 20 bytes exactly (see above)
|
|
// * toUserName and fromUserName: *max* 36 bytes, aka "up to"; null terminated
|
|
// * subject: *max* 72 bytes, aka "up to"; null terminated
|
|
// * message: Unbounded & null terminated
|
|
//
|
|
// For everything above but message, we can get away with assuming CP437
|
|
// and probably even just "ascii" for most cases. The message field is
|
|
// much more complex so we'll look for encoding kludges, detection, etc.
|
|
// later on.
|
|
//
|
|
if (msgData.modDateTime.length != 20) {
|
|
return cb(
|
|
Errors.Invalid(
|
|
`FTN packet DateTime field must be 20 bytes (got ${msgData.modDateTime.length})`
|
|
)
|
|
);
|
|
}
|
|
if (msgData.toUserName.length > 36) {
|
|
return cb(
|
|
Errors.Invalid(
|
|
`FTN packet toUserName field must be 36 bytes max (got ${msgData.toUserName.length})`
|
|
)
|
|
);
|
|
}
|
|
if (msgData.fromUserName.length > 36) {
|
|
return cb(
|
|
Errors.Invalid(
|
|
`FTN packet fromUserName field must be 36 bytes max (got ${msgData.fromUserName.length})`
|
|
)
|
|
);
|
|
}
|
|
if (msgData.subject.length > 72) {
|
|
return cb(
|
|
Errors.Invalid(
|
|
`FTN packet subject field must be 72 bytes max (got ${msgData.subject.length})`
|
|
)
|
|
);
|
|
}
|
|
|
|
// Arrays of CP437 bytes -> String
|
|
['modDateTime', 'toUserName', 'fromUserName', 'subject'].forEach(k => {
|
|
msgData[k] = strUtil.stringFromNullTermBuffer(msgData[k], 'CP437');
|
|
});
|
|
|
|
//
|
|
// The message body itself is a special beast as it may
|
|
// contain an origin line, kludges, SAUCE in the case
|
|
// of ANSI files, etc.
|
|
//
|
|
const msg = new Message({
|
|
toUserName: msgData.toUserName,
|
|
fromUserName: msgData.fromUserName,
|
|
subject: msgData.subject,
|
|
modTimestamp: ftn.getDateFromFtnDateTime(msgData.modDateTime),
|
|
});
|
|
|
|
// :TODO: When non-private (e.g. EchoMail), attempt to extract SRC from MSGID vs headers, when avail (or Orgin line? research further)
|
|
msg.meta.FtnProperty = {
|
|
ftn_orig_node: header.origNode,
|
|
ftn_dest_node: header.destNode,
|
|
ftn_orig_network: header.origNet,
|
|
ftn_dest_network: header.destNet,
|
|
|
|
ftn_attr_flags: msgData.ftn_attr_flags,
|
|
ftn_cost: msgData.ftn_cost,
|
|
|
|
ftn_msg_orig_node: msgData.ftn_msg_orig_node,
|
|
ftn_msg_dest_node: msgData.ftn_msg_dest_node,
|
|
ftn_msg_orig_net: msgData.ftn_msg_orig_net,
|
|
ftn_msg_dest_net: msgData.ftn_msg_dest_net,
|
|
};
|
|
|
|
self.processMessageBody(msgData.message, messageBodyData => {
|
|
msg.message = messageBodyData.message;
|
|
msg.meta.FtnKludge = messageBodyData.kludgeLines;
|
|
|
|
if (messageBodyData.tearLine) {
|
|
msg.meta.FtnProperty.ftn_tear_line = messageBodyData.tearLine;
|
|
|
|
if (self.options.keepTearAndOrigin) {
|
|
msg.message += `\r\n${messageBodyData.tearLine}\r\n`;
|
|
}
|
|
}
|
|
|
|
if (messageBodyData.seenBy.length > 0) {
|
|
msg.meta.FtnProperty.ftn_seen_by = messageBodyData.seenBy;
|
|
}
|
|
|
|
if (messageBodyData.area) {
|
|
msg.meta.FtnProperty.ftn_area = messageBodyData.area;
|
|
}
|
|
|
|
if (messageBodyData.originLine) {
|
|
msg.meta.FtnProperty.ftn_origin = messageBodyData.originLine;
|
|
|
|
if (self.options.keepTearAndOrigin) {
|
|
msg.message += `${messageBodyData.originLine}\r\n`;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Attempt to handle FTN time zone kludges of 'TZUTC' and
|
|
// 'TZUTCINFO'.
|
|
//
|
|
// See http://ftsc.org/docs/frl-1004.002
|
|
//
|
|
const tzKludge = msg.meta.FtnKludge.TZUTC || msg.meta.FtnKludge.TZUTCINFO;
|
|
const tzMatch = /([+-]?)([0-9]{2})([0-9]{2})/.exec(tzKludge);
|
|
if (tzMatch) {
|
|
//
|
|
// - Both kludges should provide a offset in hhmm format
|
|
// - Negative offsets must proceed with '-'
|
|
// - Positive offsets must not (to spec) proceed with '+', but
|
|
// we'll allow it.
|
|
//
|
|
const [, sign, hours, minutes] = tzMatch;
|
|
|
|
// convert to a [+|-]hh:mm format.
|
|
// example: 1300 -> +13:00
|
|
const utcOffset = `${sign || '+'}${hours}:${minutes}`;
|
|
|
|
// finally, update our modTimestamp
|
|
msg.modTimestamp = msg.modTimestamp.utcOffset(utcOffset);
|
|
}
|
|
|
|
// :TODO: Parser should give is this info:
|
|
const bytesRead =
|
|
14 + // fixed header size
|
|
msgData.modDateTime.length +
|
|
1 + // +1 = NULL
|
|
msgData.toUserName.length +
|
|
1 + // +1 = NULL
|
|
msgData.fromUserName.length +
|
|
1 + // +1 = NULL
|
|
msgData.subject.length +
|
|
1 + // +1 = NULL
|
|
msgData.message.length; // includes NULL
|
|
|
|
const nextBuf = packetBuffer.slice(bytesRead);
|
|
if (nextBuf.length > 0) {
|
|
const next = function (e) {
|
|
if (e) {
|
|
cb(e);
|
|
} else {
|
|
self.parsePacketMessages(header, nextBuf, iterator, cb);
|
|
}
|
|
};
|
|
|
|
iterator('message', msg, next);
|
|
} else {
|
|
cb(null);
|
|
}
|
|
});
|
|
};
|
|
|
|
this.sanatizeFtnProperties = function (message) {
|
|
[
|
|
Message.FtnPropertyNames.FtnOrigNode,
|
|
Message.FtnPropertyNames.FtnDestNode,
|
|
Message.FtnPropertyNames.FtnOrigNetwork,
|
|
Message.FtnPropertyNames.FtnDestNetwork,
|
|
Message.FtnPropertyNames.FtnAttrFlags,
|
|
Message.FtnPropertyNames.FtnCost,
|
|
Message.FtnPropertyNames.FtnOrigZone,
|
|
Message.FtnPropertyNames.FtnDestZone,
|
|
Message.FtnPropertyNames.FtnOrigPoint,
|
|
Message.FtnPropertyNames.FtnDestPoint,
|
|
Message.FtnPropertyNames.FtnAttribute,
|
|
Message.FtnPropertyNames.FtnMsgOrigNode,
|
|
Message.FtnPropertyNames.FtnMsgDestNode,
|
|
Message.FtnPropertyNames.FtnMsgOrigNet,
|
|
Message.FtnPropertyNames.FtnMsgDestNet,
|
|
].forEach(propName => {
|
|
if (message.meta.FtnProperty[propName]) {
|
|
message.meta.FtnProperty[propName] =
|
|
parseInt(message.meta.FtnProperty[propName]) || 0;
|
|
}
|
|
});
|
|
};
|
|
|
|
this.writeMessageHeader = function (message, buf) {
|
|
// ensure address FtnProperties are numbers
|
|
self.sanatizeFtnProperties(message);
|
|
|
|
const destNode =
|
|
message.meta.FtnProperty.ftn_msg_dest_node ||
|
|
message.meta.FtnProperty.ftn_dest_node;
|
|
const destNet =
|
|
message.meta.FtnProperty.ftn_msg_dest_net ||
|
|
message.meta.FtnProperty.ftn_dest_network;
|
|
|
|
buf.writeUInt16LE(FTN_PACKET_MESSAGE_TYPE, 0);
|
|
buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_node, 2);
|
|
buf.writeUInt16LE(destNode, 4);
|
|
buf.writeUInt16LE(message.meta.FtnProperty.ftn_orig_network, 6);
|
|
buf.writeUInt16LE(destNet, 8);
|
|
buf.writeUInt16LE(message.meta.FtnProperty.ftn_attr_flags, 10);
|
|
buf.writeUInt16LE(message.meta.FtnProperty.ftn_cost, 12);
|
|
|
|
const dateTimeBuffer = Buffer.from(
|
|
ftn.getDateTimeString(message.modTimestamp) + '\0'
|
|
);
|
|
dateTimeBuffer.copy(buf, 14);
|
|
};
|
|
|
|
this.getMessageEntryBuffer = function (message, options, cb) {
|
|
function getAppendMeta(k, m, sepChar = ':') {
|
|
let append = '';
|
|
if (m) {
|
|
let a = m;
|
|
if (!_.isArray(a)) {
|
|
a = [a];
|
|
}
|
|
a.forEach(v => {
|
|
append += `${k}${sepChar} ${v}\r`;
|
|
});
|
|
}
|
|
return append;
|
|
}
|
|
|
|
async.waterfall(
|
|
[
|
|
function prepareHeaderAndKludges(callback) {
|
|
const basicHeader = Buffer.alloc(34);
|
|
self.writeMessageHeader(message, basicHeader);
|
|
|
|
//
|
|
// To, from, and subject must be NULL term'd and have max lengths as per spec.
|
|
//
|
|
const toUserNameBuf = strUtil.stringToNullTermBuffer(
|
|
message.toUserName,
|
|
{ encoding: 'cp437', maxBufLen: 36 }
|
|
);
|
|
const fromUserNameBuf = strUtil.stringToNullTermBuffer(
|
|
message.fromUserName,
|
|
{ encoding: 'cp437', maxBufLen: 36 }
|
|
);
|
|
const subjectBuf = strUtil.stringToNullTermBuffer(message.subject, {
|
|
encoding: 'cp437',
|
|
maxBufLen: 72,
|
|
});
|
|
|
|
//
|
|
// message: unbound length, NULL term'd
|
|
//
|
|
// We need to build in various special lines - kludges, area,
|
|
// seen-by, etc.
|
|
//
|
|
let msgBody = '';
|
|
|
|
//
|
|
// 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}\r`; // note: no ^A (0x01)
|
|
}
|
|
|
|
// :TODO: DRY with similar function in this file!
|
|
Object.keys(message.meta.FtnKludge).forEach(k => {
|
|
switch (k) {
|
|
case 'PATH':
|
|
break; // skip & save for last
|
|
|
|
case 'Via':
|
|
case 'FMPT':
|
|
case 'TOPT':
|
|
case 'INTL':
|
|
msgBody += getAppendMeta(
|
|
`\x01${k}`,
|
|
message.meta.FtnKludge[k],
|
|
''
|
|
); // no sepChar
|
|
break;
|
|
|
|
default:
|
|
msgBody += getAppendMeta(
|
|
`\x01${k}`,
|
|
message.meta.FtnKludge[k]
|
|
);
|
|
break;
|
|
}
|
|
});
|
|
|
|
return callback(
|
|
null,
|
|
basicHeader,
|
|
toUserNameBuf,
|
|
fromUserNameBuf,
|
|
subjectBuf,
|
|
msgBody
|
|
);
|
|
},
|
|
function prepareAnsiMessageBody(
|
|
basicHeader,
|
|
toUserNameBuf,
|
|
fromUserNameBuf,
|
|
subjectBuf,
|
|
msgBody,
|
|
callback
|
|
) {
|
|
if (!strUtil.isAnsi(message.message)) {
|
|
return callback(
|
|
null,
|
|
basicHeader,
|
|
toUserNameBuf,
|
|
fromUserNameBuf,
|
|
subjectBuf,
|
|
msgBody,
|
|
message.message
|
|
);
|
|
}
|
|
|
|
ansiPrep(
|
|
message.message,
|
|
{
|
|
cols: 80,
|
|
rows: 'auto',
|
|
forceLineTerm: true,
|
|
exportMode: true,
|
|
},
|
|
(err, preppedMsg) => {
|
|
return callback(
|
|
null,
|
|
basicHeader,
|
|
toUserNameBuf,
|
|
fromUserNameBuf,
|
|
subjectBuf,
|
|
msgBody,
|
|
preppedMsg || message.message
|
|
);
|
|
}
|
|
);
|
|
},
|
|
function addMessageBody(
|
|
basicHeader,
|
|
toUserNameBuf,
|
|
fromUserNameBuf,
|
|
subjectBuf,
|
|
msgBody,
|
|
preppedMsg,
|
|
callback
|
|
) {
|
|
msgBody += preppedMsg + '\r';
|
|
|
|
//
|
|
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
|
|
// Tear line should be near the bottom of a message
|
|
//
|
|
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
|
|
//
|
|
msgBody += getAppendMeta(
|
|
'SEEN-BY',
|
|
message.meta.FtnProperty.ftn_seen_by
|
|
); // note: no ^A (0x01)
|
|
msgBody += getAppendMeta('\x01PATH', message.meta.FtnKludge['PATH']);
|
|
|
|
let msgBodyEncoded;
|
|
try {
|
|
msgBodyEncoded = iconv.encode(msgBody + '\0', options.encoding);
|
|
} catch (e) {
|
|
msgBodyEncoded = iconv.encode(msgBody + '\0', 'ascii');
|
|
}
|
|
|
|
return callback(
|
|
null,
|
|
Buffer.concat([
|
|
basicHeader,
|
|
toUserNameBuf,
|
|
fromUserNameBuf,
|
|
subjectBuf,
|
|
msgBodyEncoded,
|
|
])
|
|
);
|
|
},
|
|
],
|
|
(err, msgEntryBuffer) => {
|
|
return cb(err, msgEntryBuffer);
|
|
}
|
|
);
|
|
};
|
|
|
|
this.writeMessage = function (message, ws, options) {
|
|
const basicHeader = Buffer.alloc(34);
|
|
self.writeMessageHeader(message, basicHeader);
|
|
|
|
ws.write(basicHeader);
|
|
|
|
// toUserName & fromUserName: up to 36 bytes in length, NULL term'd
|
|
// :TODO: DRY...
|
|
let encBuf = iconv.encode(message.toUserName + '\0', 'CP437').slice(0, 36);
|
|
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
|
|
ws.write(encBuf);
|
|
|
|
encBuf = iconv.encode(message.fromUserName + '\0', 'CP437').slice(0, 36);
|
|
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
|
|
ws.write(encBuf);
|
|
|
|
// subject: up to 72 bytes in length, NULL term'd
|
|
encBuf = iconv.encode(message.subject + '\0', 'CP437').slice(0, 72);
|
|
encBuf[encBuf.length - 1] = '\0'; // ensure it's null term'd
|
|
ws.write(encBuf);
|
|
|
|
//
|
|
// message: unbound length, NULL term'd
|
|
//
|
|
// We need to build in various special lines - kludges, area,
|
|
// seen-by, etc.
|
|
//
|
|
// :TODO: Put this in it's own method
|
|
let msgBody = '';
|
|
|
|
function appendMeta(k, m, sepChar = ':') {
|
|
if (m) {
|
|
let a = m;
|
|
if (!_.isArray(a)) {
|
|
a = [a];
|
|
}
|
|
a.forEach(v => {
|
|
msgBody += `${k}${sepChar} ${v}\r`;
|
|
});
|
|
}
|
|
}
|
|
|
|
//
|
|
// 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}\r`; // note: no ^A (0x01)
|
|
}
|
|
|
|
Object.keys(message.meta.FtnKludge).forEach(k => {
|
|
switch (k) {
|
|
case 'PATH':
|
|
break; // skip & save for last
|
|
|
|
case 'Via':
|
|
case 'FMPT':
|
|
case 'TOPT':
|
|
case 'INTL':
|
|
appendMeta(`\x01${k}`, message.meta.FtnKludge[k], '');
|
|
break; // no sepChar
|
|
|
|
default:
|
|
appendMeta(`\x01${k}`, message.meta.FtnKludge[k]);
|
|
break;
|
|
}
|
|
});
|
|
|
|
msgBody += message.message + '\r';
|
|
|
|
//
|
|
// FTN-0004.001 @ http://ftsc.org/docs/fts-0004.001
|
|
// Tear line should be near the bottom of a message
|
|
//
|
|
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']);
|
|
|
|
//
|
|
// :TODO: We should encode based on config and add the proper kludge here!
|
|
ws.write(iconv.encode(msgBody + '\0', options.encoding));
|
|
};
|
|
|
|
this.parsePacketBuffer = function (packetBuffer, iterator, cb) {
|
|
async.waterfall(
|
|
[
|
|
function processHeader(callback) {
|
|
self.parsePacketHeader(packetBuffer, (err, header) => {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
const next = function (e) {
|
|
return callback(e, header);
|
|
};
|
|
|
|
iterator('header', header, next);
|
|
});
|
|
},
|
|
function processMessages(header, callback) {
|
|
self.parsePacketMessages(
|
|
header,
|
|
packetBuffer.slice(FTN_PACKET_HEADER_SIZE),
|
|
iterator,
|
|
callback
|
|
);
|
|
},
|
|
],
|
|
cb // complete
|
|
);
|
|
};
|
|
}
|
|
|
|
//
|
|
// Message attributes defined in FTS-0001.016
|
|
// http://ftsc.org/docs/fts-0001.016
|
|
//
|
|
// See also:
|
|
// * http://www.skepticfiles.org/aj/basics03.htm
|
|
//
|
|
Packet.Attribute = {
|
|
Private: 0x0001, // Private message / NetMail
|
|
Crash: 0x0002,
|
|
Received: 0x0004,
|
|
Sent: 0x0008,
|
|
FileAttached: 0x0010,
|
|
InTransit: 0x0020,
|
|
Orphan: 0x0040,
|
|
KillSent: 0x0080,
|
|
Local: 0x0100, // Message is from *this* system
|
|
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(
|
|
[
|
|
function getBufferIfPath(callback) {
|
|
if (_.isString(pathOrBuffer)) {
|
|
fs.readFile(pathOrBuffer, (err, data) => {
|
|
pathOrBuffer = data;
|
|
callback(err);
|
|
});
|
|
} else {
|
|
callback(null);
|
|
}
|
|
},
|
|
function parseBuffer(callback) {
|
|
self.parsePacketBuffer(pathOrBuffer, iterator, err => {
|
|
callback(err);
|
|
});
|
|
},
|
|
],
|
|
err => {
|
|
cb(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
Packet.prototype.writeHeader = function (ws, packetHeader) {
|
|
return this.writePacketHeader(packetHeader, ws);
|
|
};
|
|
|
|
Packet.prototype.writeMessageEntry = function (ws, msgEntry) {
|
|
ws.write(msgEntry);
|
|
return msgEntry.length;
|
|
};
|
|
|
|
Packet.prototype.writeTerminator = function (ws) {
|
|
//
|
|
// From FTS-0001.016:
|
|
// "A pseudo-message beginning with the word 0000H signifies the end of the packet."
|
|
//
|
|
ws.write(Buffer.from([0x00, 0x00])); // final extra null term
|
|
return 2;
|
|
};
|
|
|
|
Packet.prototype.writeStream = function (ws, messages, options) {
|
|
if (!_.isBoolean(options.terminatePacket)) {
|
|
options.terminatePacket = true;
|
|
}
|
|
|
|
if (_.isObject(options.packetHeader)) {
|
|
this.writePacketHeader(options.packetHeader, ws);
|
|
}
|
|
|
|
options.encoding = options.encoding || 'utf8';
|
|
|
|
messages.forEach(msg => {
|
|
this.writeMessage(msg, ws, options);
|
|
});
|
|
|
|
if (true === options.terminatePacket) {
|
|
ws.write(Buffer.from([0])); // final extra null term
|
|
}
|
|
};
|
|
|
|
Packet.prototype.write = function (path, packetHeader, messages, options) {
|
|
if (!_.isArray(messages)) {
|
|
messages = [messages];
|
|
}
|
|
|
|
options = options || { encoding: 'utf8' }; // utf-8 = 'CHRS UTF-8 4'
|
|
|
|
this.writeStream(
|
|
fs.createWriteStream(path), // :TODO: specify mode/etc.
|
|
messages,
|
|
Object.assign({ packetHeader: packetHeader, terminatePacket: true }, options)
|
|
);
|
|
};
|