@fluent-org/logger
Version:
A node fluent protocol compatible logger
536 lines • 19.7 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
Object.defineProperty(exports, "__esModule", { value: true });
exports.decodeEntries = exports.decodeServerStream = exports.decodeClientStream = exports.encodeMessage = exports.decodeClientMessage = exports.decodeServerMessage = exports.packEntry = exports.decode = exports.encode = exports.parseTransport = exports.generateEntry = exports.generateCompressedPackedForwardMode = exports.generatePackedForwardMode = exports.generateForwardMode = exports.generateMessageMode = exports.generateAck = exports.checkPong = exports.generatePong = exports.checkPing = exports.generatePing = exports.parseHelo = exports.generateHelo = exports.isClientMessage = exports.isServerMessage = exports.isClientTransportMessage = exports.isServerTransportMessage = exports.isClientHandshakeMessage = exports.isServerHandshakeMessage = exports.isCompressedPackedForwardMode = exports.isPackedForwardMode = exports.isForwardMode = exports.isMessageMode = exports.isEntry = exports.isEventRecord = exports.isTag = exports.isOption = exports.isTime = exports.isAck = exports.isPong = exports.isPing = exports.isHelo = void 0;
const crypto = require("crypto");
const zlib = require("zlib");
const event_time_1 = require("./event_time");
const msgpack_1 = require("@msgpack/msgpack");
const error_1 = require("./error");
// Type checking functions
// These do a lot of type validation, which is bad for performance, but avoids multiple msgpack security vulnerabilities
// The performance hit shouldn't be too bad on the client, as the only large messages the client receives are in the handshake phase
// The performance hit is worse on the server, but protects the server from DOS vulnerabilities
const isHelo = (message) => {
if (!Array.isArray(message) ||
message.length !== 2 ||
message[0] !== "HELO") {
return false;
}
if (typeof message[1] !== "object" || message[1] === null) {
return false;
}
if (typeof message[1].nonce !== "string" &&
!(typeof message[1].nonce === "object" &&
message[1].nonce instanceof Uint8Array)) {
return false;
}
if (typeof message[1].auth !== "undefined" &&
typeof message[1].auth !== "string" &&
!(typeof message[1].auth === "object" &&
message[1].auth instanceof Uint8Array)) {
return false;
}
if (typeof message[1].keepalive !== "undefined" &&
typeof message[1].keepalive !== "boolean") {
return false;
}
return true;
};
exports.isHelo = isHelo;
const isPing = (message) => {
if (!Array.isArray(message) ||
message.length !== 6 ||
message[0] !== "PING") {
return false;
}
return message.every((v, i) => i === 2
? typeof v === "string" || v instanceof Uint8Array
: typeof v === "string");
};
exports.isPing = isPing;
const isPong = (message) => {
if (!Array.isArray(message) ||
message.length !== 5 ||
message[0] !== "PONG") {
return false;
}
return message.every((v, i) => i === 1 ? typeof v === "boolean" : typeof v === "string");
};
exports.isPong = isPong;
const isAck = (message) => {
return (!!message &&
message.hasOwnProperty("ack") &&
typeof message.ack === "string");
};
exports.isAck = isAck;
const isTime = (data) => {
return typeof data === "number" || data instanceof event_time_1.default;
};
exports.isTime = isTime;
const isOption = (data) => {
if (typeof data === "undefined" || data === null) {
return true;
}
if (typeof data !== "object") {
return false;
}
if (typeof data.size !== "undefined" && typeof data.size !== "number") {
return false;
}
if (typeof data.chunk !== "undefined" && typeof data.chunk !== "string") {
return false;
}
if (typeof data.compressed !== "undefined" &&
typeof data.compressed !== "string") {
return false;
}
return true;
};
exports.isOption = isOption;
const isTag = (data) => {
return typeof data === "string";
};
exports.isTag = isTag;
const isEventRecord = (data) => {
return typeof data === "object" && data !== null;
};
exports.isEventRecord = isEventRecord;
const isEntry = (data) => {
return (Array.isArray(data) &&
data.length === 2 &&
exports.isTime(data[0]) &&
exports.isEventRecord(data[1]));
};
exports.isEntry = isEntry;
const isMessageMode = (message) => {
if (!Array.isArray(message) ||
(message.length !== 4 && message.length !== 3)) {
return false;
}
return (exports.isTag(message[0]) &&
exports.isTime(message[1]) &&
exports.isEventRecord(message[2]) &&
exports.isOption(message[3]));
};
exports.isMessageMode = isMessageMode;
const isForwardMode = (message) => {
if (!Array.isArray(message) ||
(message.length !== 3 && message.length !== 2)) {
return false;
}
return (exports.isTag(message[0]) &&
Array.isArray(message[1]) &&
message[1].every(exports.isEntry) &&
exports.isOption(message[2]));
};
exports.isForwardMode = isForwardMode;
const isPackedForwardMode = (message) => {
var _a;
if (!Array.isArray(message) ||
(message.length !== 3 && message.length !== 2)) {
return false;
}
return (exports.isTag(message[0]) &&
message[1] instanceof Uint8Array &&
exports.isOption(message[2]) &&
((_a = message[2]) === null || _a === void 0 ? void 0 : _a.compressed) !== "gzip");
};
exports.isPackedForwardMode = isPackedForwardMode;
const isCompressedPackedForwardMode = (message) => {
var _a;
if (!Array.isArray(message) || message.length !== 3) {
return false;
}
return (exports.isTag(message[0]) &&
message[1] instanceof Uint8Array &&
exports.isOption(message[2]) &&
((_a = message[2]) === null || _a === void 0 ? void 0 : _a.compressed) === "gzip");
};
exports.isCompressedPackedForwardMode = isCompressedPackedForwardMode;
const isServerHandshakeMessage = (message) => {
return exports.isHelo(message) || exports.isPong(message);
};
exports.isServerHandshakeMessage = isServerHandshakeMessage;
const isClientHandshakeMessage = (message) => {
return exports.isPing(message);
};
exports.isClientHandshakeMessage = isClientHandshakeMessage;
const isServerTransportMessage = (message) => {
return exports.isAck(message);
};
exports.isServerTransportMessage = isServerTransportMessage;
const isClientTransportMessage = (message) => {
return (exports.isMessageMode(message) ||
exports.isForwardMode(message) ||
exports.isPackedForwardMode(message) ||
exports.isCompressedPackedForwardMode(message));
};
exports.isClientTransportMessage = isClientTransportMessage;
const isServerMessage = (message) => {
return exports.isServerHandshakeMessage(message) || exports.isServerTransportMessage(message);
};
exports.isServerMessage = isServerMessage;
const isClientMessage = (message) => {
return exports.isClientHandshakeMessage(message) || exports.isClientTransportMessage(message);
};
exports.isClientMessage = isClientMessage;
const sharedKeyHash = (hostname, sharedKeyInfo) => {
return crypto
.createHash("sha512")
.update(sharedKeyInfo.salt)
.update(hostname)
.update(sharedKeyInfo.nonce)
.update(sharedKeyInfo.key)
.digest("hex");
};
const userPasswordHash = (authInfo) => {
const passwordHexDigest = crypto
.createHash("sha512")
.update(authInfo.salt)
.update(authInfo.username)
.update(authInfo.password)
.digest("hex");
return passwordHexDigest;
};
const generateHelo = (nonce, auth, keepalive) => {
// ['HELO', options(hash)]
return [
"HELO",
{
nonce,
auth,
keepalive,
},
];
};
exports.generateHelo = generateHelo;
const parseHelo = (m) => {
return m[1];
};
exports.parseHelo = parseHelo;
const generatePing = (hostname, sharedKeyInfo, authInfo) => {
const sharedKeyHexDigest = sharedKeyHash(hostname, sharedKeyInfo);
let userName = "";
let passwordHexDigest = "";
if (authInfo) {
userName = authInfo.username;
passwordHexDigest = userPasswordHash(authInfo);
}
return [
"PING",
hostname,
sharedKeyInfo.salt,
sharedKeyHexDigest,
userName,
passwordHexDigest,
];
};
exports.generatePing = generatePing;
/**
* Validates a PING message from the client
*
* Assumes a valid structure (isPing has been called)
*
* @param m The ping message to validate
* @param serverHostname The hostname of the client
* @param serverKeyInfo The key info known to the server
* @param authInfo Authentication information to validate (optional, auth not required if missing)
* @returns An object with the complete SharedKeyInfo
* @throws Error on mismatches
*/
const checkPing = (m, serverHostname, serverKeyInfo, authInfo) => {
// ['PING', self_hostname, shared_key_salt, sha512_hex(shared_key_salt + self_hostname + nonce + shared_key), username || '', sha512_hex(auth_salt + username + password) || '']
const clientHostname = m[1];
const sharedKeySalt = m[2];
const sharedKeyHexDigest = m[3];
const username = m[4];
const passwordDigest = m[5];
const serverSideDigest = sharedKeyHash(clientHostname, {
salt: sharedKeySalt,
...serverKeyInfo,
});
if (clientHostname === serverHostname) {
throw new error_1.ConfigError("Same hostname between input and output: invalid configuration");
}
if (sharedKeyHexDigest !== serverSideDigest) {
throw new error_1.SharedKeyMismatchError("shared key mismatch");
}
if (authInfo) {
if (!username) {
throw new error_1.AuthError("missing authentication information");
}
if (!authInfo.userDict.hasOwnProperty(username)) {
throw new error_1.AuthError("username/password mismatch");
}
const serverSidePasswordDigest = userPasswordHash({
salt: authInfo.salt,
username,
password: authInfo.userDict[username],
});
if (passwordDigest !== serverSidePasswordDigest) {
throw new error_1.AuthError("username/password mismatch");
}
}
return { sharedKeyInfo: { ...serverKeyInfo, salt: sharedKeySalt } };
};
exports.checkPing = checkPing;
const generatePong = (hostname, authenticated, reason, sharedKeyInfo) => {
let sharedKeyHexDigest = "";
if (authenticated && sharedKeyInfo) {
sharedKeyHexDigest = sharedKeyHash(hostname, sharedKeyInfo);
}
return ["PONG", authenticated, reason, hostname, sharedKeyHexDigest];
};
exports.generatePong = generatePong;
/**
* Checks the PONG message from the server
*
* Assumes a valid structure (isPong has been called)
* @param m The PONG message from the server to validate
* @param clientHostname The client hostname
* @param sharedKeyInfo The client shared key information
* @throws Error on validation issues
*/
const checkPong = (m, clientHostname, sharedKeyInfo) => {
// [
// 'PONG',
// bool(authentication result),
// 'reason if authentication failed',
// server_hostname,
// sha512_hex(salt + server_hostname + nonce + sharedkey)
// ]
const authResult = m[1];
const reason = m[2];
const serverHostname = m[3];
const sharedKeyHexdigest = m[4];
if (serverHostname === clientHostname) {
throw new error_1.ConfigError("Same hostname between input and output: invalid configuration");
}
if (!authResult) {
throw new error_1.AuthError(`Authentication failed: ${reason}`);
}
const clientSideHexDigest = sharedKeyHash(serverHostname, sharedKeyInfo);
if (sharedKeyHexdigest !== clientSideHexDigest) {
throw new error_1.SharedKeyMismatchError("shared key mismatch");
}
};
exports.checkPong = checkPong;
const generateAck = (ack) => {
return {
ack,
};
};
exports.generateAck = generateAck;
const maybeChunk = (chunk) => {
if (chunk) {
return { chunk };
}
else {
return {};
}
};
const generateMessageMode = (tag, time, event, chunk) => {
return [tag, time, event, maybeChunk(chunk)];
};
exports.generateMessageMode = generateMessageMode;
const generateForwardMode = (tag, entries, chunk) => {
return [tag, entries, { size: entries.length, ...maybeChunk(chunk) }];
};
exports.generateForwardMode = generateForwardMode;
const generatePackedForwardMode = (tag, packedEntries, packedEntryLength, chunk) => {
const combinedEntries = Buffer.concat(packedEntries, packedEntryLength);
const option = {
size: packedEntries.length,
...maybeChunk(chunk),
};
return [tag, combinedEntries, option];
};
exports.generatePackedForwardMode = generatePackedForwardMode;
const generateCompressedPackedForwardMode = (tag, packedEntries, packedEntryLength, chunk) => {
const combinedEntries = zlib.gzipSync(Buffer.concat(packedEntries, packedEntryLength));
const option = {
size: packedEntries.length,
...maybeChunk(chunk),
compressed: "gzip",
};
return [tag, combinedEntries, option];
};
exports.generateCompressedPackedForwardMode = generateCompressedPackedForwardMode;
const generateEntry = (time, event) => {
return [time, event];
};
exports.generateEntry = generateEntry;
/**
* Parses a transport message from the client
*
* @param message The transport message to parse
* @returns An object with the decoded entries from the object
*/
const parseTransport = (message) => {
if (exports.isMessageMode(message)) {
const options = message[3];
const tag = message[0];
const entry = [message[1], message[2]];
return {
tag,
entries: [entry],
chunk: options === null || options === void 0 ? void 0 : options.chunk,
};
}
else if (exports.isForwardMode(message)) {
const tag = message[0];
const entries = message[1];
const options = message[2];
return {
tag,
entries,
chunk: options === null || options === void 0 ? void 0 : options.chunk,
};
}
else if (exports.isPackedForwardMode(message) ||
exports.isCompressedPackedForwardMode(message)) {
const tag = message[0];
let entryBuffer = message[1];
const options = message[2];
if (exports.isCompressedPackedForwardMode(message)) {
entryBuffer = zlib.gunzipSync(entryBuffer);
}
const entries = exports.decodeEntries(entryBuffer);
return {
tag,
entries,
chunk: options === null || options === void 0 ? void 0 : options.chunk,
};
}
else {
throw new error_1.DecodeError("Expected transport message, but got something else");
}
};
exports.parseTransport = parseTransport;
/**
* The [EventTime](https://github.com/fluent/fluentd/wiki/Forward-Protocol-Specification-v1#eventtime-ext-format) ext code is 0
*/
const EVENT_TIME_EXT_TYPE = 0x00;
const extensionCodec = new msgpack_1.ExtensionCodec();
extensionCodec.register({
type: EVENT_TIME_EXT_TYPE,
encode: (object) => {
if (object instanceof event_time_1.default) {
return event_time_1.default.pack(object);
}
else {
return null;
}
},
decode: (data) => {
let buffer;
if (!Buffer.isBuffer(data)) {
buffer = Buffer.from(data);
}
else {
buffer = data;
}
return event_time_1.default.unpack(buffer);
},
});
/**
* Creates a newEncoder
*
* We can't share these because we were running into strange bugs where the internal buffers were being overwritten
* @returns A new Encoder to use
*/
const encoder = () => new msgpack_1.Encoder(extensionCodec);
/**
* Creates a new Decoder
*
* We can't share these because we were running into strange bugs where the internal buffers were being overwritten
* @returns A new Decoder to use
*/
const decoder = () => new msgpack_1.Decoder(extensionCodec);
const encode = (item) => {
return encoder().encode(item);
};
exports.encode = encode;
const decode = (data) => {
return decoder().decode(data);
};
exports.decode = decode;
const packEntry = (entry) => {
return exports.encode(entry);
};
exports.packEntry = packEntry;
const decodeServerMessage = (data) => {
return exports.decode(data);
};
exports.decodeServerMessage = decodeServerMessage;
const decodeClientMessage = (data) => {
return exports.decode(data);
};
exports.decodeClientMessage = decodeClientMessage;
const encodeMessage = (item) => {
return exports.encode(item);
};
exports.encodeMessage = encodeMessage;
/**
* Decodes a stream of data from the client
*
* @param dataStream A Readable to read the data from
* @returns An iterable of messages from the client, not type checked
*/
const decodeClientStream = (dataStream) => {
// This is a hack to avoid messagepack utf8-ization of the msgpack str format, since it mangles data.
// Fluentd, when using the out_forward plugin, will pass the data in PackedForward mode using a str to represent the packed Forward messages.
// This would normally end up getting decoded as utf8 by @msgpack/msgpack, and turned into complete garbage. This short circuits that function in the parser,
const streamDecoder = decoder();
streamDecoder._decodeUtf8String = streamDecoder.decodeUtf8String;
streamDecoder.decodeUtf8String = function (byteLength, headerOffset) {
if (this.bytes.byteLength < this.pos + headerOffset + byteLength) {
// Defer to the error handling inside the normal function, if we don't have enough data to parse
return this._decodeUtf8String(byteLength, headerOffset);
}
const offset = this.pos + headerOffset;
// If the first byte is 0x92 (fixarr of size 2), this represents a msgpack str encoded entry
// Also catch 0xdc and 0xdd, which represents arrays. This should never be passed, fixarr is more efficient, but just to cover all bases.
// If the first byte is 0x1f, then assume it is compressed
if (this.bytes[offset] === 0x92 ||
this.bytes[offset] === 0x1f ||
this.bytes[offset] === 0xdc ||
this.bytes[offset] === 0xdd) {
return this.decodeBinary(byteLength, headerOffset);
}
else {
return this._decodeUtf8String(byteLength, headerOffset);
}
}.bind(streamDecoder);
return streamDecoder.decodeStream(dataStream);
};
exports.decodeClientStream = decodeClientStream;
/**
* Decodes a stream of data from the server
*
* @param dataStream A Readable to read the data from
* @returns An iterable of messages from the server, not type checked
*/
const decodeServerStream = (dataStream) => {
return decoder().decodeStream(dataStream);
};
exports.decodeServerStream = decodeServerStream;
/**
* Decodes a sequence of entries from a Buffer
*
* Useful for PackedForward|CompressedPackedForward event modes
* @param data The data to unpack
* @returns The entries from the data
*/
const decodeEntries = (data) => {
const entries = Array.from(decoder().decodeMulti(data));
if (entries.every(exports.isEntry)) {
return entries;
}
else {
throw new error_1.DecodeError("Received invalid entries");
}
};
exports.decodeEntries = decodeEntries;
//# sourceMappingURL=protocol.js.map
;