UNPKG

pg-server

Version:

Postgres DB server emulator, proxy or honeypot

318 lines 13.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DbResponseParser = void 0; const buffer_reader_1 = require("./buffer-reader"); const responses_1 = require("./responses"); // every message is prefixed with a single bye const CODE_LENGTH = 1; // every message has an int32 length which includes itself but does // NOT include the code in the length const LEN_LENGTH = 4; const HEADER_LENGTH = CODE_LENGTH + LEN_LENGTH; const emptyBuffer = Buffer.allocUnsafe(0); class DbResponseParser { constructor(opts) { this.buffer = emptyBuffer; this.bufferLength = 0; this.bufferOffset = 0; this.reader = new buffer_reader_1.BufferReader(); if ((opts === null || opts === void 0 ? void 0 : opts.mode) === 'binary') { throw new Error('Binary mode not supported yet'); } this.mode = (opts === null || opts === void 0 ? void 0 : opts.mode) || 'text'; } process(callback, response, offset, len) { let callingback = true; let thisData; callback({ response, getRawData: () => { if (thisData) { return thisData; } if (!callingback) { throw new Error(`If you're interested in raw data, please ask for it sooner`); } return thisData = this.buffer.slice(offset, offset + len); }, }); callingback = false; } parse(buffer, callback) { this.mergeBuffer(buffer); const bufferFullLength = this.bufferOffset + this.bufferLength; let offset = this.bufferOffset; while (offset + HEADER_LENGTH <= bufferFullLength) { // code is 1 byte long - it identifies the message type const code = this.buffer[offset]; // length is 1 Uint32BE - it is the length of the message EXCLUDING the code const length = this.buffer.readUInt32BE(offset + CODE_LENGTH); const fullMessageLength = CODE_LENGTH + length; if (fullMessageLength + offset <= bufferFullLength) { const message = this.handlePacket(offset + HEADER_LENGTH, code, length, this.buffer); this.process(callback, message, offset, fullMessageLength); offset += fullMessageLength; } else { break; } } if (offset === bufferFullLength) { // No more use for the buffer this.buffer = emptyBuffer; this.bufferLength = 0; this.bufferOffset = 0; } else { // Adjust the cursors of remainingBuffer this.bufferLength = bufferFullLength - offset; this.bufferOffset = offset; } } mergeBuffer(buffer) { if (this.bufferLength > 0) { const newLength = this.bufferLength + buffer.byteLength; const newFullLength = newLength + this.bufferOffset; if (newFullLength > this.buffer.byteLength) { // We can't concat the new buffer with the remaining one let newBuffer; if (newLength <= this.buffer.byteLength && this.bufferOffset >= this.bufferLength) { // We can move the relevant part to the beginning of the buffer instead of allocating a new buffer newBuffer = this.buffer; } else { // Allocate a new larger buffer let newBufferLength = this.buffer.byteLength * 2; while (newLength >= newBufferLength) { newBufferLength *= 2; } newBuffer = Buffer.allocUnsafe(newBufferLength); } // Move the remaining buffer to the new one this.buffer.copy(newBuffer, 0, this.bufferOffset, this.bufferOffset + this.bufferLength); this.buffer = newBuffer; this.bufferOffset = 0; } // Concat the new buffer with the remaining one buffer.copy(this.buffer, this.bufferOffset + this.bufferLength); this.bufferLength = newLength; } else { this.buffer = buffer; this.bufferOffset = 0; this.bufferLength = buffer.byteLength; } } handlePacket(offset, type, length, bytes) { switch (type) { case responses_1.ResponseCode.BindComplete: case responses_1.ResponseCode.ParseComplete: case responses_1.ResponseCode.CloseComplete: case responses_1.ResponseCode.NoData: case responses_1.ResponseCode.PortalSuspended: case responses_1.ResponseCode.CopyDone: case responses_1.ResponseCode.ReplicationStart: case responses_1.ResponseCode.EmptyQuery: return { type }; case responses_1.ResponseCode.DataRow: return this.parseDataRowMessage(offset, length, bytes); case responses_1.ResponseCode.CommandComplete: return this.parseCommandCompleteMessage(offset, length, bytes); case responses_1.ResponseCode.ReadyForQuery: return this.parseReadyForQueryMessage(offset, length, bytes); case responses_1.ResponseCode.NotificationResponse: return this.parseNotificationMessage(offset, length, bytes); case responses_1.ResponseCode.AuthenticationResponse: return this.parseAuthenticationResponse(offset, length, bytes); case responses_1.ResponseCode.ParameterStatus: return this.parseParameterStatusMessage(offset, length, bytes); case responses_1.ResponseCode.BackendKeyData: return this.parseBackendKeyData(offset, length, bytes); case responses_1.ResponseCode.ErrorMessage: return this.parseErrorMessage(offset, length, bytes, responses_1.ResponseCode.ErrorMessage); case responses_1.ResponseCode.NoticeMessage: return this.parseErrorMessage(offset, length, bytes, responses_1.ResponseCode.NoticeMessage); case responses_1.ResponseCode.RowDescriptionMessage: return this.parseRowDescriptionMessage(offset, length, bytes); case responses_1.ResponseCode.CopyIn: case responses_1.ResponseCode.CopyOut: return this.parseCopyMessage(offset, length, bytes, type); case responses_1.ResponseCode.CopyData: return this.parseCopyData(offset, length, bytes); default: throw new Error(`unknown message code: 0x${type.toString(16)}`); } } parseReadyForQueryMessage(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const status = this.reader.string(1); return { type: responses_1.ResponseCode.ReadyForQuery, status, }; } parseCommandCompleteMessage(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const text = this.reader.cstring(); return { type: responses_1.ResponseCode.CommandComplete, text, }; } parseCopyData(offset, length, bytes) { const data = bytes.slice(offset, offset + (length - 4)); return { type: responses_1.ResponseCode.CopyData, data, }; } parseCopyMessage(offset, length, bytes, type) { this.reader.setBuffer(offset, bytes); const isBinary = this.reader.byte() !== 0; const columnCount = this.reader.int16(); const columnTypes = Array(columnCount); for (let i = 0; i < columnCount; i++) { columnTypes[i] = this.reader.int16(); } return { type, columnTypes, isBinary, }; } parseNotificationMessage(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const processId = this.reader.int32(); const channel = this.reader.cstring(); const payload = this.reader.cstring(); return { type: responses_1.ResponseCode.NotificationResponse, processId, channel, payload }; } parseRowDescriptionMessage(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const fieldCount = this.reader.int16(); const fields = Array(fieldCount); for (let i = 0; i < fieldCount; i++) { fields[i] = this.parseField(); } return { type: responses_1.ResponseCode.RowDescriptionMessage, fields, }; } parseField() { const name = this.reader.cstring(); const tableID = this.reader.int32(); const columnID = this.reader.int16(); const dataTypeID = this.reader.int32(); const dataTypeSize = this.reader.int16(); const dataTypeModifier = this.reader.int32(); const mode = this.reader.int16() === 0 ? 'text' : 'binary'; return { name, tableID, columnID, dataTypeID, dataTypeSize, dataTypeModifier, mode }; } parseDataRowMessage(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const fieldCount = this.reader.int16(); const fields = new Array(fieldCount); for (let i = 0; i < fieldCount; i++) { const len = this.reader.int32(); // a -1 for length means the value of the field is null fields[i] = len === -1 ? null : this.reader.string(len); } return { type: responses_1.ResponseCode.DataRow, fields, }; } parseParameterStatusMessage(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const name = this.reader.cstring(); const value = this.reader.cstring(); return { type: responses_1.ResponseCode.ParameterStatus, name, value }; } parseBackendKeyData(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const processID = this.reader.int32(); const secretKey = this.reader.int32(); return { type: responses_1.ResponseCode.BackendKeyData, processID, secretKey }; } parseAuthenticationResponse(offset, length, bytes) { this.reader.setBuffer(offset, bytes); const code = this.reader.int32(); const ret = { type: responses_1.ResponseCode.AuthenticationResponse }; switch (code) { case 0: // AuthenticationOk return { ...ret, kind: 'ok' }; case 3: // AuthenticationCleartextPassword if (length === 8) { return { ...ret, kind: 'cleartextPassword' }; } return { ...ret, kind: 'ok' }; case 5: // AuthenticationMD5Password if (length === 12) { const salt = this.reader.bytes(4); return { ...ret, salt, kind: 'md5Password' }; } return { ...ret, kind: 'ok' }; case 10: // AuthenticationSASL const mechanisms = []; let mechanism; do { mechanism = this.reader.cstring(); if (mechanism) { mechanisms.push(mechanism); } } while (mechanism); return { ...ret, mechanisms, kind: 'SASL' }; case 11: // AuthenticationSASLContinue return { ...ret, data: this.reader.string(length - 8), kind: 'SASLContinue' }; case 12: // AuthenticationSASLFinal return { ...ret, data: this.reader.string(length - 8), kind: 'SASLFinal' }; default: throw new Error('Unknown authenticationOk message type ' + code); } } parseErrorMessage(offset, length, bytes, type) { this.reader.setBuffer(offset, bytes); const fields = {}; let fieldType = this.reader.string(1); while (fieldType !== '\0') { fields[fieldType] = this.reader.cstring(); fieldType = this.reader.string(1); } const message = { message: fields.M, severity: fields.S, code: fields.C, detail: fields.D, hint: fields.H, position: fields.P, internalPosition: fields.p, internalQuery: fields.q, where: fields.W, schema: fields.s, table: fields.t, column: fields.c, dataType: fields.d, constraint: fields.n, file: fields.F, line: fields.L, routine: fields.R, }; return { message, type }; } } exports.DbResponseParser = DbResponseParser; //# sourceMappingURL=response-parser.js.map