UNPKG

@fluent-org/logger

Version:
536 lines 19.7 kB
"use strict"; /* 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